summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/bb')
-rw-r--r--bitbake/lib/bb/COW.py323
-rw-r--r--bitbake/lib/bb/__init__.py146
-rw-r--r--bitbake/lib/bb/build.py667
-rw-r--r--bitbake/lib/bb/cache.py847
-rw-r--r--bitbake/lib/bb/cache_extra.py75
-rw-r--r--bitbake/lib/bb/checksum.py90
-rw-r--r--bitbake/lib/bb/codeparser.py319
-rw-r--r--bitbake/lib/bb/command.py441
-rw-r--r--bitbake/lib/bb/compat.py6
-rw-r--r--bitbake/lib/bb/cooker.py1838
-rw-r--r--bitbake/lib/bb/cookerdata.py304
-rw-r--r--bitbake/lib/bb/daemonize.py190
-rw-r--r--bitbake/lib/bb/data.py375
-rw-r--r--bitbake/lib/bb/data_smart.py794
-rw-r--r--bitbake/lib/bb/event.py635
-rw-r--r--bitbake/lib/bb/exceptions.py91
-rw-r--r--bitbake/lib/bb/fetch2/__init__.py1538
-rw-r--r--bitbake/lib/bb/fetch2/bzr.py143
-rw-r--r--bitbake/lib/bb/fetch2/cvs.py171
-rw-r--r--bitbake/lib/bb/fetch2/git.py326
-rw-r--r--bitbake/lib/bb/fetch2/gitsm.py78
-rw-r--r--bitbake/lib/bb/fetch2/hg.py181
-rw-r--r--bitbake/lib/bb/fetch2/local.py116
-rw-r--r--bitbake/lib/bb/fetch2/osc.py135
-rw-r--r--bitbake/lib/bb/fetch2/perforce.py198
-rw-r--r--bitbake/lib/bb/fetch2/repo.py98
-rw-r--r--bitbake/lib/bb/fetch2/sftp.py129
-rw-r--r--bitbake/lib/bb/fetch2/ssh.py127
-rw-r--r--bitbake/lib/bb/fetch2/svk.py97
-rw-r--r--bitbake/lib/bb/fetch2/svn.py189
-rw-r--r--bitbake/lib/bb/fetch2/wget.py97
-rw-r--r--bitbake/lib/bb/methodpool.py29
-rw-r--r--bitbake/lib/bb/monitordisk.py265
-rw-r--r--bitbake/lib/bb/msg.py182
-rw-r--r--bitbake/lib/bb/namedtuple_with_abc.py255
-rw-r--r--bitbake/lib/bb/parse/__init__.py146
-rw-r--r--bitbake/lib/bb/parse/ast.py482
-rw-r--r--bitbake/lib/bb/parse/parse_py/BBHandler.py258
-rw-r--r--bitbake/lib/bb/parse/parse_py/ConfHandler.py182
-rw-r--r--bitbake/lib/bb/parse/parse_py/__init__.py33
-rw-r--r--bitbake/lib/bb/persist_data.py215
-rw-r--r--bitbake/lib/bb/process.py133
-rw-r--r--bitbake/lib/bb/providers.py381
-rw-r--r--bitbake/lib/bb/pysh/__init__.py0
-rw-r--r--bitbake/lib/bb/pysh/builtin.py710
-rw-r--r--bitbake/lib/bb/pysh/interp.py1367
-rw-r--r--bitbake/lib/bb/pysh/lsprof.py116
-rw-r--r--bitbake/lib/bb/pysh/pysh.py167
-rw-r--r--bitbake/lib/bb/pysh/pyshlex.py888
-rw-r--r--bitbake/lib/bb/pysh/pyshyacc.py779
-rw-r--r--bitbake/lib/bb/pysh/sherrors.py41
-rw-r--r--bitbake/lib/bb/pysh/subprocess_fix.py77
-rw-r--r--bitbake/lib/bb/runqueue.py1914
-rw-r--r--bitbake/lib/bb/server/__init__.py96
-rw-r--r--bitbake/lib/bb/server/process.py223
-rw-r--r--bitbake/lib/bb/server/xmlrpc.py365
-rw-r--r--bitbake/lib/bb/shell.py820
-rw-r--r--bitbake/lib/bb/siggen.py445
-rw-r--r--bitbake/lib/bb/taskdata.py645
-rw-r--r--bitbake/lib/bb/tests/__init__.py0
-rw-r--r--bitbake/lib/bb/tests/codeparser.py374
-rw-r--r--bitbake/lib/bb/tests/cow.py136
-rw-r--r--bitbake/lib/bb/tests/data.py254
-rw-r--r--bitbake/lib/bb/tests/fetch.py424
-rw-r--r--bitbake/lib/bb/tests/utils.py53
-rw-r--r--bitbake/lib/bb/tinfoil.py96
-rw-r--r--bitbake/lib/bb/ui/__init__.py17
-rw-r--r--bitbake/lib/bb/ui/crumbs/__init__.py17
-rwxr-xr-xbitbake/lib/bb/ui/crumbs/builddetailspage.py436
-rwxr-xr-xbitbake/lib/bb/ui/crumbs/builder.py1476
-rw-r--r--bitbake/lib/bb/ui/crumbs/buildmanager.py455
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/__init__.py0
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py340
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py44
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py95
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py215
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py172
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py296
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py163
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/propertydialog.py451
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py90
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py51
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py160
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py122
-rw-r--r--bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py893
-rw-r--r--bitbake/lib/bb/ui/crumbs/hobcolor.py38
-rw-r--r--bitbake/lib/bb/ui/crumbs/hobeventhandler.py624
-rw-r--r--bitbake/lib/bb/ui/crumbs/hoblistmodel.py900
-rwxr-xr-xbitbake/lib/bb/ui/crumbs/hobpages.py128
-rw-r--r--bitbake/lib/bb/ui/crumbs/hobwidget.py904
-rw-r--r--bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py559
-rwxr-xr-xbitbake/lib/bb/ui/crumbs/imagedetailspage.py669
-rwxr-xr-xbitbake/lib/bb/ui/crumbs/packageselectionpage.py355
-rw-r--r--bitbake/lib/bb/ui/crumbs/persistenttooltip.py186
-rw-r--r--bitbake/lib/bb/ui/crumbs/progress.py23
-rw-r--r--bitbake/lib/bb/ui/crumbs/progressbar.py59
-rw-r--r--bitbake/lib/bb/ui/crumbs/puccho.glade606
-rwxr-xr-xbitbake/lib/bb/ui/crumbs/recipeselectionpage.py335
-rw-r--r--bitbake/lib/bb/ui/crumbs/runningbuild.py533
-rw-r--r--bitbake/lib/bb/ui/crumbs/sanitycheckpage.py85
-rw-r--r--bitbake/lib/bb/ui/crumbs/utils.py34
-rw-r--r--bitbake/lib/bb/ui/depexp.py326
-rw-r--r--bitbake/lib/bb/ui/goggle.py121
-rwxr-xr-xbitbake/lib/bb/ui/hob.py109
-rw-r--r--bitbake/lib/bb/ui/icons/images/images_display.pngbin0 -> 6898 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/images/images_hover.pngbin0 -> 7051 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/add-hover.pngbin0 -> 1212 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/add.pngbin0 -> 1176 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/alert.pngbin0 -> 3954 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/confirmation.pngbin0 -> 5789 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/denied.pngbin0 -> 3955 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/error.pngbin0 -> 6482 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/info.pngbin0 -> 3311 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/issues.pngbin0 -> 4549 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/refresh.pngbin0 -> 5250 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/remove-hover.pngbin0 -> 2809 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/remove.pngbin0 -> 1971 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/indicators/tick.pngbin0 -> 4563 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/info/info_display.pngbin0 -> 4117 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/info/info_hover.pngbin0 -> 4167 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/layers/layers_display.pngbin0 -> 4840 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/layers/layers_hover.pngbin0 -> 5257 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/packages/packages_display.pngbin0 -> 7011 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/packages/packages_hover.pngbin0 -> 7121 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/recipe/recipe_display.pngbin0 -> 4723 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/recipe/recipe_hover.pngbin0 -> 4866 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/settings/settings_display.pngbin0 -> 6076 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/settings/settings_hover.pngbin0 -> 6269 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/templates/templates_display.pngbin0 -> 5651 bytes
-rw-r--r--bitbake/lib/bb/ui/icons/templates/templates_hover.pngbin0 -> 5791 bytes
-rw-r--r--bitbake/lib/bb/ui/knotty.py541
-rw-r--r--bitbake/lib/bb/ui/ncurses.py373
-rw-r--r--bitbake/lib/bb/ui/puccho.py425
-rw-r--r--bitbake/lib/bb/ui/uievent.py133
-rw-r--r--bitbake/lib/bb/ui/uihelper.py100
-rw-r--r--bitbake/lib/bb/utils.py869
136 files changed, 37843 insertions, 0 deletions
diff --git a/bitbake/lib/bb/COW.py b/bitbake/lib/bb/COW.py
new file mode 100644
index 0000000000..6917ec378a
--- /dev/null
+++ b/bitbake/lib/bb/COW.py
@@ -0,0 +1,323 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# This is a copy on write dictionary and set which abuses classes to try and be nice and fast.
5#
6# Copyright (C) 2006 Tim Amsell
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21#Please Note:
22# Be careful when using mutable types (ie Dict and Lists) - operations involving these are SLOW.
23# Assign a file to __warn__ to get warnings about slow operations.
24#
25
26from __future__ import print_function
27import copy
28import types
29ImmutableTypes = (
30 types.NoneType,
31 bool,
32 complex,
33 float,
34 int,
35 long,
36 tuple,
37 frozenset,
38 basestring
39)
40
41MUTABLE = "__mutable__"
42
43class COWMeta(type):
44 pass
45
46class COWDictMeta(COWMeta):
47 __warn__ = False
48 __hasmutable__ = False
49 __marker__ = tuple()
50
51 def __str__(cls):
52 # FIXME: I have magic numbers!
53 return "<COWDict Level: %i Current Keys: %i>" % (cls.__count__, len(cls.__dict__) - 3)
54 __repr__ = __str__
55
56 def cow(cls):
57 class C(cls):
58 __count__ = cls.__count__ + 1
59 return C
60 copy = cow
61 __call__ = cow
62
63 def __setitem__(cls, key, value):
64 if not isinstance(value, ImmutableTypes):
65 if not isinstance(value, COWMeta):
66 cls.__hasmutable__ = True
67 key += MUTABLE
68 setattr(cls, key, value)
69
70 def __getmutable__(cls, key, readonly=False):
71 nkey = key + MUTABLE
72 try:
73 return cls.__dict__[nkey]
74 except KeyError:
75 pass
76
77 value = getattr(cls, nkey)
78 if readonly:
79 return value
80
81 if not cls.__warn__ is False and not isinstance(value, COWMeta):
82 print("Warning: Doing a copy because %s is a mutable type." % key, file=cls.__warn__)
83 try:
84 value = value.copy()
85 except AttributeError as e:
86 value = copy.copy(value)
87 setattr(cls, nkey, value)
88 return value
89
90 __getmarker__ = []
91 def __getreadonly__(cls, key, default=__getmarker__):
92 """\
93 Get a value (even if mutable) which you promise not to change.
94 """
95 return cls.__getitem__(key, default, True)
96
97 def __getitem__(cls, key, default=__getmarker__, readonly=False):
98 try:
99 try:
100 value = getattr(cls, key)
101 except AttributeError:
102 value = cls.__getmutable__(key, readonly)
103
104 # This is for values which have been deleted
105 if value is cls.__marker__:
106 raise AttributeError("key %s does not exist." % key)
107
108 return value
109 except AttributeError as e:
110 if not default is cls.__getmarker__:
111 return default
112
113 raise KeyError(str(e))
114
115 def __delitem__(cls, key):
116 cls.__setitem__(key, cls.__marker__)
117
118 def __revertitem__(cls, key):
119 if not cls.__dict__.has_key(key):
120 key += MUTABLE
121 delattr(cls, key)
122
123 def __contains__(cls, key):
124 return cls.has_key(key)
125
126 def has_key(cls, key):
127 value = cls.__getreadonly__(key, cls.__marker__)
128 if value is cls.__marker__:
129 return False
130 return True
131
132 def iter(cls, type, readonly=False):
133 for key in dir(cls):
134 if key.startswith("__"):
135 continue
136
137 if key.endswith(MUTABLE):
138 key = key[:-len(MUTABLE)]
139
140 if type == "keys":
141 yield key
142
143 try:
144 if readonly:
145 value = cls.__getreadonly__(key)
146 else:
147 value = cls[key]
148 except KeyError:
149 continue
150
151 if type == "values":
152 yield value
153 if type == "items":
154 yield (key, value)
155 raise StopIteration()
156
157 def iterkeys(cls):
158 return cls.iter("keys")
159 def itervalues(cls, readonly=False):
160 if not cls.__warn__ is False and cls.__hasmutable__ and readonly is False:
161 print("Warning: If you arn't going to change any of the values call with True.", file=cls.__warn__)
162 return cls.iter("values", readonly)
163 def iteritems(cls, readonly=False):
164 if not cls.__warn__ is False and cls.__hasmutable__ and readonly is False:
165 print("Warning: If you arn't going to change any of the values call with True.", file=cls.__warn__)
166 return cls.iter("items", readonly)
167
168class COWSetMeta(COWDictMeta):
169 def __str__(cls):
170 # FIXME: I have magic numbers!
171 return "<COWSet Level: %i Current Keys: %i>" % (cls.__count__, len(cls.__dict__) -3)
172 __repr__ = __str__
173
174 def cow(cls):
175 class C(cls):
176 __count__ = cls.__count__ + 1
177 return C
178
179 def add(cls, value):
180 COWDictMeta.__setitem__(cls, repr(hash(value)), value)
181
182 def remove(cls, value):
183 COWDictMeta.__delitem__(cls, repr(hash(value)))
184
185 def __in__(cls, value):
186 return COWDictMeta.has_key(repr(hash(value)))
187
188 def iterkeys(cls):
189 raise TypeError("sets don't have keys")
190
191 def iteritems(cls):
192 raise TypeError("sets don't have 'items'")
193
194# These are the actual classes you use!
195class COWDictBase(object):
196 __metaclass__ = COWDictMeta
197 __count__ = 0
198
199class COWSetBase(object):
200 __metaclass__ = COWSetMeta
201 __count__ = 0
202
203if __name__ == "__main__":
204 import sys
205 COWDictBase.__warn__ = sys.stderr
206 a = COWDictBase()
207 print("a", a)
208
209 a['a'] = 'a'
210 a['b'] = 'b'
211 a['dict'] = {}
212
213 b = a.copy()
214 print("b", b)
215 b['c'] = 'b'
216
217 print()
218
219 print("a", a)
220 for x in a.iteritems():
221 print(x)
222 print("--")
223 print("b", b)
224 for x in b.iteritems():
225 print(x)
226 print()
227
228 b['dict']['a'] = 'b'
229 b['a'] = 'c'
230
231 print("a", a)
232 for x in a.iteritems():
233 print(x)
234 print("--")
235 print("b", b)
236 for x in b.iteritems():
237 print(x)
238 print()
239
240 try:
241 b['dict2']
242 except KeyError as e:
243 print("Okay!")
244
245 a['set'] = COWSetBase()
246 a['set'].add("o1")
247 a['set'].add("o1")
248 a['set'].add("o2")
249
250 print("a", a)
251 for x in a['set'].itervalues():
252 print(x)
253 print("--")
254 print("b", b)
255 for x in b['set'].itervalues():
256 print(x)
257 print()
258
259 b['set'].add('o3')
260
261 print("a", a)
262 for x in a['set'].itervalues():
263 print(x)
264 print("--")
265 print("b", b)
266 for x in b['set'].itervalues():
267 print(x)
268 print()
269
270 a['set2'] = set()
271 a['set2'].add("o1")
272 a['set2'].add("o1")
273 a['set2'].add("o2")
274
275 print("a", a)
276 for x in a.iteritems():
277 print(x)
278 print("--")
279 print("b", b)
280 for x in b.iteritems(readonly=True):
281 print(x)
282 print()
283
284 del b['b']
285 try:
286 print(b['b'])
287 except KeyError:
288 print("Yay! deleted key raises error")
289
290 if b.has_key('b'):
291 print("Boo!")
292 else:
293 print("Yay - has_key with delete works!")
294
295 print("a", a)
296 for x in a.iteritems():
297 print(x)
298 print("--")
299 print("b", b)
300 for x in b.iteritems(readonly=True):
301 print(x)
302 print()
303
304 b.__revertitem__('b')
305
306 print("a", a)
307 for x in a.iteritems():
308 print(x)
309 print("--")
310 print("b", b)
311 for x in b.iteritems(readonly=True):
312 print(x)
313 print()
314
315 b.__revertitem__('dict')
316 print("a", a)
317 for x in a.iteritems():
318 print(x)
319 print("--")
320 print("b", b)
321 for x in b.iteritems(readonly=True):
322 print(x)
323 print()
diff --git a/bitbake/lib/bb/__init__.py b/bitbake/lib/bb/__init__.py
new file mode 100644
index 0000000000..f4907bbd72
--- /dev/null
+++ b/bitbake/lib/bb/__init__.py
@@ -0,0 +1,146 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake Build System Python Library
5#
6# Copyright (C) 2003 Holger Schurig
7# Copyright (C) 2003, 2004 Chris Larson
8#
9# Based on Gentoo's portage.py.
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License version 2 as
13# published by the Free Software Foundation.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License along
21# with this program; if not, write to the Free Software Foundation, Inc.,
22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
24__version__ = "1.20.0"
25
26import sys
27if sys.version_info < (2, 7, 3):
28 raise RuntimeError("Sorry, python 2.7.3 or later is required for this version of bitbake")
29
30
31class BBHandledException(Exception):
32 """
33 The big dilemma for generic bitbake code is what information to give the user
34 when an exception occurs. Any exception inheriting this base exception class
35 has already provided information to the user via some 'fired' message type such as
36 an explicitly fired event using bb.fire, or a bb.error message. If bitbake
37 encounters an exception derived from this class, no backtrace or other information
38 will be given to the user, its assumed the earlier event provided the relevant information.
39 """
40 pass
41
42import os
43import logging
44
45
46class NullHandler(logging.Handler):
47 def emit(self, record):
48 pass
49
50Logger = logging.getLoggerClass()
51class BBLogger(Logger):
52 def __init__(self, name):
53 if name.split(".")[0] == "BitBake":
54 self.debug = self.bbdebug
55 Logger.__init__(self, name)
56
57 def bbdebug(self, level, msg, *args, **kwargs):
58 return self.log(logging.DEBUG - level + 1, msg, *args, **kwargs)
59
60 def plain(self, msg, *args, **kwargs):
61 return self.log(logging.INFO + 1, msg, *args, **kwargs)
62
63 def verbose(self, msg, *args, **kwargs):
64 return self.log(logging.INFO - 1, msg, *args, **kwargs)
65
66logging.raiseExceptions = False
67logging.setLoggerClass(BBLogger)
68
69logger = logging.getLogger("BitBake")
70logger.addHandler(NullHandler())
71logger.setLevel(logging.DEBUG - 2)
72
73# This has to be imported after the setLoggerClass, as the import of bb.msg
74# can result in construction of the various loggers.
75import bb.msg
76
77from bb import fetch2 as fetch
78sys.modules['bb.fetch'] = sys.modules['bb.fetch2']
79
80# Messaging convenience functions
81def plain(*args):
82 logger.plain(''.join(args))
83
84def debug(lvl, *args):
85 if isinstance(lvl, basestring):
86 logger.warn("Passed invalid debug level '%s' to bb.debug", lvl)
87 args = (lvl,) + args
88 lvl = 1
89 logger.debug(lvl, ''.join(args))
90
91def note(*args):
92 logger.info(''.join(args))
93
94def warn(*args):
95 logger.warn(''.join(args))
96
97def error(*args):
98 logger.error(''.join(args))
99
100def fatal(*args):
101 logger.critical(''.join(args))
102 sys.exit(1)
103
104
105def deprecated(func, name=None, advice=""):
106 """This is a decorator which can be used to mark functions
107 as deprecated. It will result in a warning being emmitted
108 when the function is used."""
109 import warnings
110
111 if advice:
112 advice = ": %s" % advice
113 if name is None:
114 name = func.__name__
115
116 def newFunc(*args, **kwargs):
117 warnings.warn("Call to deprecated function %s%s." % (name,
118 advice),
119 category=DeprecationWarning,
120 stacklevel=2)
121 return func(*args, **kwargs)
122 newFunc.__name__ = func.__name__
123 newFunc.__doc__ = func.__doc__
124 newFunc.__dict__.update(func.__dict__)
125 return newFunc
126
127# For compatibility
128def deprecate_import(current, modulename, fromlist, renames = None):
129 """Import objects from one module into another, wrapping them with a DeprecationWarning"""
130 import sys
131
132 module = __import__(modulename, fromlist = fromlist)
133 for position, objname in enumerate(fromlist):
134 obj = getattr(module, objname)
135 newobj = deprecated(obj, "{0}.{1}".format(current, objname),
136 "Please use {0}.{1} instead".format(modulename, objname))
137 if renames:
138 newname = renames[position]
139 else:
140 newname = objname
141
142 setattr(sys.modules[current], newname, newobj)
143
144deprecate_import(__name__, "bb.fetch", ("MalformedUrl", "encodeurl", "decodeurl"))
145deprecate_import(__name__, "bb.utils", ("mkdirhier", "movefile", "copyfile", "which"))
146deprecate_import(__name__, "bb.utils", ["vercmp_string"], ["vercmp"])
diff --git a/bitbake/lib/bb/build.py b/bitbake/lib/bb/build.py
new file mode 100644
index 0000000000..2e49a09365
--- /dev/null
+++ b/bitbake/lib/bb/build.py
@@ -0,0 +1,667 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake 'Build' implementation
5#
6# Core code for function execution and task handling in the
7# BitBake build tools.
8#
9# Copyright (C) 2003, 2004 Chris Larson
10#
11# Based on Gentoo's portage.py.
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26#Based on functions from the base bb module, Copyright 2003 Holger Schurig
27
28import os
29import sys
30import logging
31import shlex
32import glob
33import bb
34import bb.msg
35import bb.process
36from contextlib import nested
37from bb import event, utils
38
39bblogger = logging.getLogger('BitBake')
40logger = logging.getLogger('BitBake.Build')
41
42NULL = open(os.devnull, 'r+')
43
44
45# When we execute a python function we'd like certain things
46# in all namespaces, hence we add them to __builtins__
47# If we do not do this and use the exec globals, they will
48# not be available to subfunctions.
49__builtins__['bb'] = bb
50__builtins__['os'] = os
51
52class FuncFailed(Exception):
53 def __init__(self, name = None, logfile = None):
54 self.logfile = logfile
55 self.name = name
56 if name:
57 self.msg = 'Function failed: %s' % name
58 else:
59 self.msg = "Function failed"
60
61 def __str__(self):
62 if self.logfile and os.path.exists(self.logfile):
63 msg = ("%s (log file is located at %s)" %
64 (self.msg, self.logfile))
65 else:
66 msg = self.msg
67 return msg
68
69class TaskBase(event.Event):
70 """Base class for task events"""
71
72 def __init__(self, t, logfile, d):
73 self._task = t
74 self._package = d.getVar("PF", True)
75 self.taskfile = d.getVar("FILE", True)
76 self.taskname = self._task
77 self.logfile = logfile
78 event.Event.__init__(self)
79 self._message = "recipe %s: task %s: %s" % (d.getVar("PF", True), t, self.getDisplayName())
80
81 def getTask(self):
82 return self._task
83
84 def setTask(self, task):
85 self._task = task
86
87 def getDisplayName(self):
88 return bb.event.getName(self)[4:]
89
90 task = property(getTask, setTask, None, "task property")
91
92class TaskStarted(TaskBase):
93 """Task execution started"""
94
95class TaskSucceeded(TaskBase):
96 """Task execution completed"""
97
98class TaskFailed(TaskBase):
99 """Task execution failed"""
100
101 def __init__(self, task, logfile, metadata, errprinted = False):
102 self.errprinted = errprinted
103 super(TaskFailed, self).__init__(task, logfile, metadata)
104
105class TaskFailedSilent(TaskBase):
106 """Task execution failed (silently)"""
107 def getDisplayName(self):
108 # Don't need to tell the user it was silent
109 return "Failed"
110
111class TaskInvalid(TaskBase):
112
113 def __init__(self, task, metadata):
114 super(TaskInvalid, self).__init__(task, None, metadata)
115 self._message = "No such task '%s'" % task
116
117
118class LogTee(object):
119 def __init__(self, logger, outfile):
120 self.outfile = outfile
121 self.logger = logger
122 self.name = self.outfile.name
123
124 def write(self, string):
125 self.logger.plain(string)
126 self.outfile.write(string)
127
128 def __enter__(self):
129 self.outfile.__enter__()
130 return self
131
132 def __exit__(self, *excinfo):
133 self.outfile.__exit__(*excinfo)
134
135 def __repr__(self):
136 return '<LogTee {0}>'.format(self.name)
137 def flush(self):
138 self.outfile.flush()
139
140def exec_func(func, d, dirs = None):
141 """Execute an BB 'function'"""
142
143 body = d.getVar(func)
144 if not body:
145 if body is None:
146 logger.warn("Function %s doesn't exist", func)
147 return
148
149 flags = d.getVarFlags(func)
150 cleandirs = flags.get('cleandirs')
151 if cleandirs:
152 for cdir in d.expand(cleandirs).split():
153 bb.utils.remove(cdir, True)
154
155 if dirs is None:
156 dirs = flags.get('dirs')
157 if dirs:
158 dirs = d.expand(dirs).split()
159
160 if dirs:
161 for adir in dirs:
162 bb.utils.mkdirhier(adir)
163 adir = dirs[-1]
164 else:
165 adir = d.getVar('B', True)
166 bb.utils.mkdirhier(adir)
167
168 ispython = flags.get('python')
169
170 lockflag = flags.get('lockfiles')
171 if lockflag:
172 lockfiles = [d.expand(f) for f in lockflag.split()]
173 else:
174 lockfiles = None
175
176 tempdir = d.getVar('T', True)
177
178 # or func allows items to be executed outside of the normal
179 # task set, such as buildhistory
180 task = d.getVar('BB_RUNTASK', True) or func
181 if task == func:
182 taskfunc = task
183 else:
184 taskfunc = "%s.%s" % (task, func)
185
186 runfmt = d.getVar('BB_RUNFMT', True) or "run.{func}.{pid}"
187 runfn = runfmt.format(taskfunc=taskfunc, task=task, func=func, pid=os.getpid())
188 runfile = os.path.join(tempdir, runfn)
189 bb.utils.mkdirhier(os.path.dirname(runfile))
190
191 # Setup the courtesy link to the runfn, only for tasks
192 # we create the link 'just' before the run script is created
193 # if we create it after, and if the run script fails, then the
194 # link won't be created as an exception would be fired.
195 if task == func:
196 runlink = os.path.join(tempdir, 'run.{0}'.format(task))
197 if runlink:
198 bb.utils.remove(runlink)
199
200 try:
201 os.symlink(runfn, runlink)
202 except OSError:
203 pass
204
205 with bb.utils.fileslocked(lockfiles):
206 if ispython:
207 exec_func_python(func, d, runfile, cwd=adir)
208 else:
209 exec_func_shell(func, d, runfile, cwd=adir)
210
211_functionfmt = """
212def {function}(d):
213{body}
214
215{function}(d)
216"""
217logformatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
218def exec_func_python(func, d, runfile, cwd=None):
219 """Execute a python BB 'function'"""
220
221 bbfile = d.getVar('FILE', True)
222 code = _functionfmt.format(function=func, body=d.getVar(func, True))
223 bb.utils.mkdirhier(os.path.dirname(runfile))
224 with open(runfile, 'w') as script:
225 script.write(code)
226
227 if cwd:
228 try:
229 olddir = os.getcwd()
230 except OSError:
231 olddir = None
232 os.chdir(cwd)
233
234 bb.debug(2, "Executing python function %s" % func)
235
236 try:
237 comp = utils.better_compile(code, func, bbfile)
238 utils.better_exec(comp, {"d": d}, code, bbfile)
239 except:
240 if sys.exc_info()[0] in (bb.parse.SkipPackage, bb.build.FuncFailed):
241 raise
242
243 raise FuncFailed(func, None)
244 finally:
245 bb.debug(2, "Python function %s finished" % func)
246
247 if cwd and olddir:
248 try:
249 os.chdir(olddir)
250 except OSError:
251 pass
252
253def exec_func_shell(func, d, runfile, cwd=None):
254 """Execute a shell function from the metadata
255
256 Note on directory behavior. The 'dirs' varflag should contain a list
257 of the directories you need created prior to execution. The last
258 item in the list is where we will chdir/cd to.
259 """
260
261 # Don't let the emitted shell script override PWD
262 d.delVarFlag('PWD', 'export')
263
264 with open(runfile, 'w') as script:
265 script.write('''#!/bin/sh\n
266# Emit a useful diagnostic if something fails:
267bb_exit_handler() {
268 ret=$?
269 case $ret in
270 0) ;;
271 *) case $BASH_VERSION in
272 "") echo "WARNING: exit code $ret from a shell command.";;
273 *) echo "WARNING: ${BASH_SOURCE[0]}:${BASH_LINENO[0]} exit $ret from
274 \"$BASH_COMMAND\"";;
275 esac
276 exit $ret
277 esac
278}
279trap 'bb_exit_handler' 0
280set -e
281''')
282
283 bb.data.emit_func(func, script, d)
284
285 if bb.msg.loggerVerboseLogs:
286 script.write("set -x\n")
287 if cwd:
288 script.write("cd %s\n" % cwd)
289 script.write("%s\n" % func)
290 script.write('''
291# cleanup
292ret=$?
293trap '' 0
294exit $?
295''')
296
297 os.chmod(runfile, 0775)
298
299 cmd = runfile
300 if d.getVarFlag(func, 'fakeroot'):
301 fakerootcmd = d.getVar('FAKEROOT', True)
302 if fakerootcmd:
303 cmd = [fakerootcmd, runfile]
304
305 if bb.msg.loggerDefaultVerbose:
306 logfile = LogTee(logger, sys.stdout)
307 else:
308 logfile = sys.stdout
309
310 bb.debug(2, "Executing shell function %s" % func)
311
312 try:
313 with open(os.devnull, 'r+') as stdin:
314 bb.process.run(cmd, shell=False, stdin=stdin, log=logfile)
315 except bb.process.CmdError:
316 logfn = d.getVar('BB_LOGFILE', True)
317 raise FuncFailed(func, logfn)
318
319 bb.debug(2, "Shell function %s finished" % func)
320
321def _task_data(fn, task, d):
322 localdata = bb.data.createCopy(d)
323 localdata.setVar('BB_FILENAME', fn)
324 localdata.setVar('BB_CURRENTTASK', task[3:])
325 localdata.setVar('OVERRIDES', 'task-%s:%s' %
326 (task[3:], d.getVar('OVERRIDES', False)))
327 localdata.finalize()
328 bb.data.expandKeys(localdata)
329 return localdata
330
331def _exec_task(fn, task, d, quieterr):
332 """Execute a BB 'task'
333
334 Execution of a task involves a bit more setup than executing a function,
335 running it with its own local metadata, and with some useful variables set.
336 """
337 if not d.getVarFlag(task, 'task'):
338 event.fire(TaskInvalid(task, d), d)
339 logger.error("No such task: %s" % task)
340 return 1
341
342 logger.debug(1, "Executing task %s", task)
343
344 localdata = _task_data(fn, task, d)
345 tempdir = localdata.getVar('T', True)
346 if not tempdir:
347 bb.fatal("T variable not set, unable to build")
348
349 # Change nice level if we're asked to
350 nice = localdata.getVar("BB_TASK_NICE_LEVEL", True)
351 if nice:
352 curnice = os.nice(0)
353 nice = int(nice) - curnice
354 newnice = os.nice(nice)
355 logger.debug(1, "Renice to %s " % newnice)
356
357 bb.utils.mkdirhier(tempdir)
358
359 # Determine the logfile to generate
360 logfmt = localdata.getVar('BB_LOGFMT', True) or 'log.{task}.{pid}'
361 logbase = logfmt.format(task=task, pid=os.getpid())
362
363 # Document the order of the tasks...
364 logorder = os.path.join(tempdir, 'log.task_order')
365 try:
366 with open(logorder, 'a') as logorderfile:
367 logorderfile.write('{0} ({1}): {2}\n'.format(task, os.getpid(), logbase))
368 except OSError:
369 logger.exception("Opening log file '%s'", logorder)
370 pass
371
372 # Setup the courtesy link to the logfn
373 loglink = os.path.join(tempdir, 'log.{0}'.format(task))
374 logfn = os.path.join(tempdir, logbase)
375 if loglink:
376 bb.utils.remove(loglink)
377
378 try:
379 os.symlink(logbase, loglink)
380 except OSError:
381 pass
382
383 prefuncs = localdata.getVarFlag(task, 'prefuncs', expand=True)
384 postfuncs = localdata.getVarFlag(task, 'postfuncs', expand=True)
385
386 class ErrorCheckHandler(logging.Handler):
387 def __init__(self):
388 self.triggered = False
389 logging.Handler.__init__(self, logging.ERROR)
390 def emit(self, record):
391 self.triggered = True
392
393 # Handle logfiles
394 si = open('/dev/null', 'r')
395 try:
396 bb.utils.mkdirhier(os.path.dirname(logfn))
397 logfile = open(logfn, 'w')
398 except OSError:
399 logger.exception("Opening log file '%s'", logfn)
400 pass
401
402 # Dup the existing fds so we dont lose them
403 osi = [os.dup(sys.stdin.fileno()), sys.stdin.fileno()]
404 oso = [os.dup(sys.stdout.fileno()), sys.stdout.fileno()]
405 ose = [os.dup(sys.stderr.fileno()), sys.stderr.fileno()]
406
407 # Replace those fds with our own
408 os.dup2(si.fileno(), osi[1])
409 os.dup2(logfile.fileno(), oso[1])
410 os.dup2(logfile.fileno(), ose[1])
411
412 # Ensure python logging goes to the logfile
413 handler = logging.StreamHandler(logfile)
414 handler.setFormatter(logformatter)
415 # Always enable full debug output into task logfiles
416 handler.setLevel(logging.DEBUG - 2)
417 bblogger.addHandler(handler)
418
419 errchk = ErrorCheckHandler()
420 bblogger.addHandler(errchk)
421
422 localdata.setVar('BB_LOGFILE', logfn)
423 localdata.setVar('BB_RUNTASK', task)
424
425 event.fire(TaskStarted(task, logfn, localdata), localdata)
426 try:
427 for func in (prefuncs or '').split():
428 exec_func(func, localdata)
429 exec_func(task, localdata)
430 for func in (postfuncs or '').split():
431 exec_func(func, localdata)
432 except FuncFailed as exc:
433 if quieterr:
434 event.fire(TaskFailedSilent(task, logfn, localdata), localdata)
435 else:
436 errprinted = errchk.triggered
437 logger.error(str(exc))
438 event.fire(TaskFailed(task, logfn, localdata, errprinted), localdata)
439 return 1
440 finally:
441 sys.stdout.flush()
442 sys.stderr.flush()
443
444 bblogger.removeHandler(handler)
445
446 # Restore the backup fds
447 os.dup2(osi[0], osi[1])
448 os.dup2(oso[0], oso[1])
449 os.dup2(ose[0], ose[1])
450
451 # Close the backup fds
452 os.close(osi[0])
453 os.close(oso[0])
454 os.close(ose[0])
455 si.close()
456
457 logfile.close()
458 if os.path.exists(logfn) and os.path.getsize(logfn) == 0:
459 logger.debug(2, "Zero size logfn %s, removing", logfn)
460 bb.utils.remove(logfn)
461 bb.utils.remove(loglink)
462 event.fire(TaskSucceeded(task, logfn, localdata), localdata)
463
464 if not localdata.getVarFlag(task, 'nostamp') and not localdata.getVarFlag(task, 'selfstamp'):
465 make_stamp(task, localdata)
466
467 return 0
468
469def exec_task(fn, task, d, profile = False):
470 try:
471 quieterr = False
472 if d.getVarFlag(task, "quieterrors") is not None:
473 quieterr = True
474
475 if profile:
476 profname = "profile-%s.log" % (d.getVar("PN", True) + "-" + task)
477 try:
478 import cProfile as profile
479 except:
480 import profile
481 prof = profile.Profile()
482 ret = profile.Profile.runcall(prof, _exec_task, fn, task, d, quieterr)
483 prof.dump_stats(profname)
484 bb.utils.process_profilelog(profname)
485
486 return ret
487 else:
488 return _exec_task(fn, task, d, quieterr)
489
490 except Exception:
491 from traceback import format_exc
492 if not quieterr:
493 logger.error("Build of %s failed" % (task))
494 logger.error(format_exc())
495 failedevent = TaskFailed(task, None, d, True)
496 event.fire(failedevent, d)
497 return 1
498
499def stamp_internal(taskname, d, file_name):
500 """
501 Internal stamp helper function
502 Makes sure the stamp directory exists
503 Returns the stamp path+filename
504
505 In the bitbake core, d can be a CacheData and file_name will be set.
506 When called in task context, d will be a data store, file_name will not be set
507 """
508 taskflagname = taskname
509 if taskname.endswith("_setscene") and taskname != "do_setscene":
510 taskflagname = taskname.replace("_setscene", "")
511
512 if file_name:
513 stamp = d.stamp_base[file_name].get(taskflagname) or d.stamp[file_name]
514 extrainfo = d.stamp_extrainfo[file_name].get(taskflagname) or ""
515 else:
516 stamp = d.getVarFlag(taskflagname, 'stamp-base', True) or d.getVar('STAMP', True)
517 file_name = d.getVar('BB_FILENAME', True)
518 extrainfo = d.getVarFlag(taskflagname, 'stamp-extra-info', True) or ""
519
520 if not stamp:
521 return
522
523 stamp = bb.parse.siggen.stampfile(stamp, file_name, taskname, extrainfo)
524
525 stampdir = os.path.dirname(stamp)
526 if bb.parse.cached_mtime_noerror(stampdir) == 0:
527 bb.utils.mkdirhier(stampdir)
528
529 return stamp
530
531def stamp_cleanmask_internal(taskname, d, file_name):
532 """
533 Internal stamp helper function to generate stamp cleaning mask
534 Returns the stamp path+filename
535
536 In the bitbake core, d can be a CacheData and file_name will be set.
537 When called in task context, d will be a data store, file_name will not be set
538 """
539 taskflagname = taskname
540 if taskname.endswith("_setscene") and taskname != "do_setscene":
541 taskflagname = taskname.replace("_setscene", "")
542
543 if file_name:
544 stamp = d.stamp_base_clean[file_name].get(taskflagname) or d.stampclean[file_name]
545 extrainfo = d.stamp_extrainfo[file_name].get(taskflagname) or ""
546 else:
547 stamp = d.getVarFlag(taskflagname, 'stamp-base-clean', True) or d.getVar('STAMPCLEAN', True)
548 file_name = d.getVar('BB_FILENAME', True)
549 extrainfo = d.getVarFlag(taskflagname, 'stamp-extra-info', True) or ""
550
551 if not stamp:
552 return []
553
554 cleanmask = bb.parse.siggen.stampcleanmask(stamp, file_name, taskname, extrainfo)
555
556 return [cleanmask, cleanmask.replace(taskflagname, taskflagname + "_setscene")]
557
558def make_stamp(task, d, file_name = None):
559 """
560 Creates/updates a stamp for a given task
561 (d can be a data dict or dataCache)
562 """
563 cleanmask = stamp_cleanmask_internal(task, d, file_name)
564 for mask in cleanmask:
565 for name in glob.glob(mask):
566 # Preserve sigdata files in the stamps directory
567 if "sigdata" in name:
568 continue
569 # Preserve taint files in the stamps directory
570 if name.endswith('.taint'):
571 continue
572 os.unlink(name)
573
574 stamp = stamp_internal(task, d, file_name)
575 # Remove the file and recreate to force timestamp
576 # change on broken NFS filesystems
577 if stamp:
578 bb.utils.remove(stamp)
579 open(stamp, "w").close()
580
581 # If we're in task context, write out a signature file for each task
582 # as it completes
583 if not task.endswith("_setscene") and task != "do_setscene" and not file_name:
584 file_name = d.getVar('BB_FILENAME', True)
585 bb.parse.siggen.dump_sigtask(file_name, task, d.getVar('STAMP', True), True)
586
587def del_stamp(task, d, file_name = None):
588 """
589 Removes a stamp for a given task
590 (d can be a data dict or dataCache)
591 """
592 stamp = stamp_internal(task, d, file_name)
593 bb.utils.remove(stamp)
594
595def write_taint(task, d, file_name = None):
596 """
597 Creates a "taint" file which will force the specified task and its
598 dependents to be re-run the next time by influencing the value of its
599 taskhash.
600 (d can be a data dict or dataCache)
601 """
602 import uuid
603 if file_name:
604 taintfn = d.stamp[file_name] + '.' + task + '.taint'
605 else:
606 taintfn = d.getVar('STAMP', True) + '.' + task + '.taint'
607 bb.utils.mkdirhier(os.path.dirname(taintfn))
608 # The specific content of the taint file is not really important,
609 # we just need it to be random, so a random UUID is used
610 with open(taintfn, 'w') as taintf:
611 taintf.write(str(uuid.uuid4()))
612
613def stampfile(taskname, d, file_name = None):
614 """
615 Return the stamp for a given task
616 (d can be a data dict or dataCache)
617 """
618 return stamp_internal(taskname, d, file_name)
619
620def add_tasks(tasklist, d):
621 task_deps = d.getVar('_task_deps')
622 if not task_deps:
623 task_deps = {}
624 if not 'tasks' in task_deps:
625 task_deps['tasks'] = []
626 if not 'parents' in task_deps:
627 task_deps['parents'] = {}
628
629 for task in tasklist:
630 task = d.expand(task)
631 d.setVarFlag(task, 'task', 1)
632
633 if not task in task_deps['tasks']:
634 task_deps['tasks'].append(task)
635
636 flags = d.getVarFlags(task)
637 def getTask(name):
638 if not name in task_deps:
639 task_deps[name] = {}
640 if name in flags:
641 deptask = d.expand(flags[name])
642 task_deps[name][task] = deptask
643 getTask('depends')
644 getTask('rdepends')
645 getTask('deptask')
646 getTask('rdeptask')
647 getTask('recrdeptask')
648 getTask('recideptask')
649 getTask('nostamp')
650 getTask('fakeroot')
651 getTask('noexec')
652 getTask('umask')
653 task_deps['parents'][task] = []
654 if 'deps' in flags:
655 for dep in flags['deps']:
656 dep = d.expand(dep)
657 task_deps['parents'][task].append(dep)
658
659 # don't assume holding a reference
660 d.setVar('_task_deps', task_deps)
661
662def remove_task(task, kill, d):
663 """Remove an BB 'task'.
664
665 If kill is 1, also remove tasks that depend on this task."""
666
667 d.delVarFlag(task, 'task')
diff --git a/bitbake/lib/bb/cache.py b/bitbake/lib/bb/cache.py
new file mode 100644
index 0000000000..318781ba9b
--- /dev/null
+++ b/bitbake/lib/bb/cache.py
@@ -0,0 +1,847 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake Cache implementation
5#
6# Caching of bitbake variables before task execution
7
8# Copyright (C) 2006 Richard Purdie
9# Copyright (C) 2012 Intel Corporation
10
11# but small sections based on code from bin/bitbake:
12# Copyright (C) 2003, 2004 Chris Larson
13# Copyright (C) 2003, 2004 Phil Blundell
14# Copyright (C) 2003 - 2005 Michael 'Mickey' Lauer
15# Copyright (C) 2005 Holger Hans Peter Freyther
16# Copyright (C) 2005 ROAD GmbH
17#
18# This program is free software; you can redistribute it and/or modify
19# it under the terms of the GNU General Public License version 2 as
20# published by the Free Software Foundation.
21#
22# This program is distributed in the hope that it will be useful,
23# but WITHOUT ANY WARRANTY; without even the implied warranty of
24# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25# GNU General Public License for more details.
26#
27# You should have received a copy of the GNU General Public License along
28# with this program; if not, write to the Free Software Foundation, Inc.,
29# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
30
31
32import os
33import logging
34from collections import defaultdict
35import bb.utils
36
37logger = logging.getLogger("BitBake.Cache")
38
39try:
40 import cPickle as pickle
41except ImportError:
42 import pickle
43 logger.info("Importing cPickle failed. "
44 "Falling back to a very slow implementation.")
45
46__cache_version__ = "147"
47
48def getCacheFile(path, filename, data_hash):
49 return os.path.join(path, filename + "." + data_hash)
50
51# RecipeInfoCommon defines common data retrieving methods
52# from meta data for caches. CoreRecipeInfo as well as other
53# Extra RecipeInfo needs to inherit this class
54class RecipeInfoCommon(object):
55
56 @classmethod
57 def listvar(cls, var, metadata):
58 return cls.getvar(var, metadata).split()
59
60 @classmethod
61 def intvar(cls, var, metadata):
62 return int(cls.getvar(var, metadata) or 0)
63
64 @classmethod
65 def depvar(cls, var, metadata):
66 return bb.utils.explode_deps(cls.getvar(var, metadata))
67
68 @classmethod
69 def pkgvar(cls, var, packages, metadata):
70 return dict((pkg, cls.depvar("%s_%s" % (var, pkg), metadata))
71 for pkg in packages)
72
73 @classmethod
74 def taskvar(cls, var, tasks, metadata):
75 return dict((task, cls.getvar("%s_task-%s" % (var, task), metadata))
76 for task in tasks)
77
78 @classmethod
79 def flaglist(cls, flag, varlist, metadata, squash=False):
80 out_dict = dict((var, metadata.getVarFlag(var, flag, True))
81 for var in varlist)
82 if squash:
83 return dict((k,v) for (k,v) in out_dict.iteritems() if v)
84 else:
85 return out_dict
86
87 @classmethod
88 def getvar(cls, var, metadata):
89 return metadata.getVar(var, True) or ''
90
91
92class CoreRecipeInfo(RecipeInfoCommon):
93 __slots__ = ()
94
95 cachefile = "bb_cache.dat"
96
97 def __init__(self, filename, metadata):
98 self.file_depends = metadata.getVar('__depends', False)
99 self.timestamp = bb.parse.cached_mtime(filename)
100 self.variants = self.listvar('__VARIANTS', metadata) + ['']
101 self.appends = self.listvar('__BBAPPEND', metadata)
102 self.nocache = self.getvar('__BB_DONT_CACHE', metadata)
103
104 self.skipreason = self.getvar('__SKIPPED', metadata)
105 if self.skipreason:
106 self.pn = self.getvar('PN', metadata) or bb.parse.BBHandler.vars_from_file(filename,metadata)[0]
107 self.skipped = True
108 self.provides = self.depvar('PROVIDES', metadata)
109 self.rprovides = self.depvar('RPROVIDES', metadata)
110 return
111
112 self.tasks = metadata.getVar('__BBTASKS', False)
113
114 self.pn = self.getvar('PN', metadata)
115 self.packages = self.listvar('PACKAGES', metadata)
116 if not self.pn in self.packages:
117 self.packages.append(self.pn)
118
119 self.basetaskhashes = self.taskvar('BB_BASEHASH', self.tasks, metadata)
120 self.hashfilename = self.getvar('BB_HASHFILENAME', metadata)
121
122 self.task_deps = metadata.getVar('_task_deps', False) or {'tasks': [], 'parents': {}}
123
124 self.skipped = False
125 self.pe = self.getvar('PE', metadata)
126 self.pv = self.getvar('PV', metadata)
127 self.pr = self.getvar('PR', metadata)
128 self.defaultpref = self.intvar('DEFAULT_PREFERENCE', metadata)
129 self.not_world = self.getvar('EXCLUDE_FROM_WORLD', metadata)
130 self.stamp = self.getvar('STAMP', metadata)
131 self.stampclean = self.getvar('STAMPCLEAN', metadata)
132 self.stamp_base = self.flaglist('stamp-base', self.tasks, metadata)
133 self.stamp_base_clean = self.flaglist('stamp-base-clean', self.tasks, metadata)
134 self.stamp_extrainfo = self.flaglist('stamp-extra-info', self.tasks, metadata)
135 self.file_checksums = self.flaglist('file-checksums', self.tasks, metadata, True)
136 self.packages_dynamic = self.listvar('PACKAGES_DYNAMIC', metadata)
137 self.depends = self.depvar('DEPENDS', metadata)
138 self.provides = self.depvar('PROVIDES', metadata)
139 self.rdepends = self.depvar('RDEPENDS', metadata)
140 self.rprovides = self.depvar('RPROVIDES', metadata)
141 self.rrecommends = self.depvar('RRECOMMENDS', metadata)
142 self.rprovides_pkg = self.pkgvar('RPROVIDES', self.packages, metadata)
143 self.rdepends_pkg = self.pkgvar('RDEPENDS', self.packages, metadata)
144 self.rrecommends_pkg = self.pkgvar('RRECOMMENDS', self.packages, metadata)
145 self.inherits = self.getvar('__inherit_cache', metadata)
146 self.fakerootenv = self.getvar('FAKEROOTENV', metadata)
147 self.fakerootdirs = self.getvar('FAKEROOTDIRS', metadata)
148 self.fakerootnoenv = self.getvar('FAKEROOTNOENV', metadata)
149
150 @classmethod
151 def init_cacheData(cls, cachedata):
152 # CacheData in Core RecipeInfo Class
153 cachedata.task_deps = {}
154 cachedata.pkg_fn = {}
155 cachedata.pkg_pn = defaultdict(list)
156 cachedata.pkg_pepvpr = {}
157 cachedata.pkg_dp = {}
158
159 cachedata.stamp = {}
160 cachedata.stampclean = {}
161 cachedata.stamp_base = {}
162 cachedata.stamp_base_clean = {}
163 cachedata.stamp_extrainfo = {}
164 cachedata.file_checksums = {}
165 cachedata.fn_provides = {}
166 cachedata.pn_provides = defaultdict(list)
167 cachedata.all_depends = []
168
169 cachedata.deps = defaultdict(list)
170 cachedata.packages = defaultdict(list)
171 cachedata.providers = defaultdict(list)
172 cachedata.rproviders = defaultdict(list)
173 cachedata.packages_dynamic = defaultdict(list)
174
175 cachedata.rundeps = defaultdict(lambda: defaultdict(list))
176 cachedata.runrecs = defaultdict(lambda: defaultdict(list))
177 cachedata.possible_world = []
178 cachedata.universe_target = []
179 cachedata.hashfn = {}
180
181 cachedata.basetaskhash = {}
182 cachedata.inherits = {}
183 cachedata.fakerootenv = {}
184 cachedata.fakerootnoenv = {}
185 cachedata.fakerootdirs = {}
186
187 def add_cacheData(self, cachedata, fn):
188 cachedata.task_deps[fn] = self.task_deps
189 cachedata.pkg_fn[fn] = self.pn
190 cachedata.pkg_pn[self.pn].append(fn)
191 cachedata.pkg_pepvpr[fn] = (self.pe, self.pv, self.pr)
192 cachedata.pkg_dp[fn] = self.defaultpref
193 cachedata.stamp[fn] = self.stamp
194 cachedata.stampclean[fn] = self.stampclean
195 cachedata.stamp_base[fn] = self.stamp_base
196 cachedata.stamp_base_clean[fn] = self.stamp_base_clean
197 cachedata.stamp_extrainfo[fn] = self.stamp_extrainfo
198 cachedata.file_checksums[fn] = self.file_checksums
199
200 provides = [self.pn]
201 for provide in self.provides:
202 if provide not in provides:
203 provides.append(provide)
204 cachedata.fn_provides[fn] = provides
205
206 for provide in provides:
207 cachedata.providers[provide].append(fn)
208 if provide not in cachedata.pn_provides[self.pn]:
209 cachedata.pn_provides[self.pn].append(provide)
210
211 for dep in self.depends:
212 if dep not in cachedata.deps[fn]:
213 cachedata.deps[fn].append(dep)
214 if dep not in cachedata.all_depends:
215 cachedata.all_depends.append(dep)
216
217 rprovides = self.rprovides
218 for package in self.packages:
219 cachedata.packages[package].append(fn)
220 rprovides += self.rprovides_pkg[package]
221
222 for rprovide in rprovides:
223 cachedata.rproviders[rprovide].append(fn)
224
225 for package in self.packages_dynamic:
226 cachedata.packages_dynamic[package].append(fn)
227
228 # Build hash of runtime depends and rececommends
229 for package in self.packages + [self.pn]:
230 cachedata.rundeps[fn][package] = list(self.rdepends) + self.rdepends_pkg[package]
231 cachedata.runrecs[fn][package] = list(self.rrecommends) + self.rrecommends_pkg[package]
232
233 # Collect files we may need for possible world-dep
234 # calculations
235 if not self.not_world:
236 cachedata.possible_world.append(fn)
237
238 # create a collection of all targets for sanity checking
239 # tasks, such as upstream versions, license, and tools for
240 # task and image creation.
241 cachedata.universe_target.append(self.pn)
242
243 cachedata.hashfn[fn] = self.hashfilename
244 for task, taskhash in self.basetaskhashes.iteritems():
245 identifier = '%s.%s' % (fn, task)
246 cachedata.basetaskhash[identifier] = taskhash
247
248 cachedata.inherits[fn] = self.inherits
249 cachedata.fakerootenv[fn] = self.fakerootenv
250 cachedata.fakerootnoenv[fn] = self.fakerootnoenv
251 cachedata.fakerootdirs[fn] = self.fakerootdirs
252
253
254
255class Cache(object):
256 """
257 BitBake Cache implementation
258 """
259
260 def __init__(self, data, data_hash, caches_array):
261 # Pass caches_array information into Cache Constructor
262 # It will be used in later for deciding whether we
263 # need extra cache file dump/load support
264 self.caches_array = caches_array
265 self.cachedir = data.getVar("CACHE", True)
266 self.clean = set()
267 self.checked = set()
268 self.depends_cache = {}
269 self.data = None
270 self.data_fn = None
271 self.cacheclean = True
272 self.data_hash = data_hash
273
274 if self.cachedir in [None, '']:
275 self.has_cache = False
276 logger.info("Not using a cache. "
277 "Set CACHE = <directory> to enable.")
278 return
279
280 self.has_cache = True
281 self.cachefile = getCacheFile(self.cachedir, "bb_cache.dat", self.data_hash)
282
283 logger.debug(1, "Using cache in '%s'", self.cachedir)
284 bb.utils.mkdirhier(self.cachedir)
285
286 cache_ok = True
287 if self.caches_array:
288 for cache_class in self.caches_array:
289 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
290 cachefile = getCacheFile(self.cachedir, cache_class.cachefile, self.data_hash)
291 cache_ok = cache_ok and os.path.exists(cachefile)
292 cache_class.init_cacheData(self)
293 if cache_ok:
294 self.load_cachefile()
295 elif os.path.isfile(self.cachefile):
296 logger.info("Out of date cache found, rebuilding...")
297
298 def load_cachefile(self):
299 # Firstly, using core cache file information for
300 # valid checking
301 with open(self.cachefile, "rb") as cachefile:
302 pickled = pickle.Unpickler(cachefile)
303 try:
304 cache_ver = pickled.load()
305 bitbake_ver = pickled.load()
306 except Exception:
307 logger.info('Invalid cache, rebuilding...')
308 return
309
310 if cache_ver != __cache_version__:
311 logger.info('Cache version mismatch, rebuilding...')
312 return
313 elif bitbake_ver != bb.__version__:
314 logger.info('Bitbake version mismatch, rebuilding...')
315 return
316
317
318 cachesize = 0
319 previous_progress = 0
320 previous_percent = 0
321
322 # Calculate the correct cachesize of all those cache files
323 for cache_class in self.caches_array:
324 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
325 cachefile = getCacheFile(self.cachedir, cache_class.cachefile, self.data_hash)
326 with open(cachefile, "rb") as cachefile:
327 cachesize += os.fstat(cachefile.fileno()).st_size
328
329 bb.event.fire(bb.event.CacheLoadStarted(cachesize), self.data)
330
331 for cache_class in self.caches_array:
332 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
333 cachefile = getCacheFile(self.cachedir, cache_class.cachefile, self.data_hash)
334 with open(cachefile, "rb") as cachefile:
335 pickled = pickle.Unpickler(cachefile)
336 while cachefile:
337 try:
338 key = pickled.load()
339 value = pickled.load()
340 except Exception:
341 break
342 if self.depends_cache.has_key(key):
343 self.depends_cache[key].append(value)
344 else:
345 self.depends_cache[key] = [value]
346 # only fire events on even percentage boundaries
347 current_progress = cachefile.tell() + previous_progress
348 current_percent = 100 * current_progress / cachesize
349 if current_percent > previous_percent:
350 previous_percent = current_percent
351 bb.event.fire(bb.event.CacheLoadProgress(current_progress, cachesize),
352 self.data)
353
354 previous_progress += current_progress
355
356 # Note: depends cache number is corresponding to the parsing file numbers.
357 # The same file has several caches, still regarded as one item in the cache
358 bb.event.fire(bb.event.CacheLoadCompleted(cachesize,
359 len(self.depends_cache)),
360 self.data)
361
362
363 @staticmethod
364 def virtualfn2realfn(virtualfn):
365 """
366 Convert a virtual file name to a real one + the associated subclass keyword
367 """
368
369 fn = virtualfn
370 cls = ""
371 if virtualfn.startswith('virtual:'):
372 elems = virtualfn.split(':')
373 cls = ":".join(elems[1:-1])
374 fn = elems[-1]
375 return (fn, cls)
376
377 @staticmethod
378 def realfn2virtual(realfn, cls):
379 """
380 Convert a real filename + the associated subclass keyword to a virtual filename
381 """
382 if cls == "":
383 return realfn
384 return "virtual:" + cls + ":" + realfn
385
386 @classmethod
387 def loadDataFull(cls, virtualfn, appends, cfgData):
388 """
389 Return a complete set of data for fn.
390 To do this, we need to parse the file.
391 """
392
393 (fn, virtual) = cls.virtualfn2realfn(virtualfn)
394
395 logger.debug(1, "Parsing %s (full)", fn)
396
397 cfgData.setVar("__ONLYFINALISE", virtual or "default")
398 bb_data = cls.load_bbfile(fn, appends, cfgData)
399 return bb_data[virtual]
400
401 @classmethod
402 def parse(cls, filename, appends, configdata, caches_array):
403 """Parse the specified filename, returning the recipe information"""
404 infos = []
405 datastores = cls.load_bbfile(filename, appends, configdata)
406 depends = []
407 for variant, data in sorted(datastores.iteritems(),
408 key=lambda i: i[0],
409 reverse=True):
410 virtualfn = cls.realfn2virtual(filename, variant)
411 depends = depends + (data.getVar("__depends", False) or [])
412 if depends and not variant:
413 data.setVar("__depends", depends)
414
415 info_array = []
416 for cache_class in caches_array:
417 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
418 info = cache_class(filename, data)
419 info_array.append(info)
420 infos.append((virtualfn, info_array))
421
422 return infos
423
424 def load(self, filename, appends, configdata):
425 """Obtain the recipe information for the specified filename,
426 using cached values if available, otherwise parsing.
427
428 Note that if it does parse to obtain the info, it will not
429 automatically add the information to the cache or to your
430 CacheData. Use the add or add_info method to do so after
431 running this, or use loadData instead."""
432 cached = self.cacheValid(filename, appends)
433 if cached:
434 infos = []
435 # info_array item is a list of [CoreRecipeInfo, XXXRecipeInfo]
436 info_array = self.depends_cache[filename]
437 for variant in info_array[0].variants:
438 virtualfn = self.realfn2virtual(filename, variant)
439 infos.append((virtualfn, self.depends_cache[virtualfn]))
440 else:
441 logger.debug(1, "Parsing %s", filename)
442 return self.parse(filename, appends, configdata, self.caches_array)
443
444 return cached, infos
445
446 def loadData(self, fn, appends, cfgData, cacheData):
447 """Load the recipe info for the specified filename,
448 parsing and adding to the cache if necessary, and adding
449 the recipe information to the supplied CacheData instance."""
450 skipped, virtuals = 0, 0
451
452 cached, infos = self.load(fn, appends, cfgData)
453 for virtualfn, info_array in infos:
454 if info_array[0].skipped:
455 logger.debug(1, "Skipping %s: %s", virtualfn, info_array[0].skipreason)
456 skipped += 1
457 else:
458 self.add_info(virtualfn, info_array, cacheData, not cached)
459 virtuals += 1
460
461 return cached, skipped, virtuals
462
463 def cacheValid(self, fn, appends):
464 """
465 Is the cache valid for fn?
466 Fast version, no timestamps checked.
467 """
468 if fn not in self.checked:
469 self.cacheValidUpdate(fn, appends)
470
471 # Is cache enabled?
472 if not self.has_cache:
473 return False
474 if fn in self.clean:
475 return True
476 return False
477
478 def cacheValidUpdate(self, fn, appends):
479 """
480 Is the cache valid for fn?
481 Make thorough (slower) checks including timestamps.
482 """
483 # Is cache enabled?
484 if not self.has_cache:
485 return False
486
487 self.checked.add(fn)
488
489 # File isn't in depends_cache
490 if not fn in self.depends_cache:
491 logger.debug(2, "Cache: %s is not cached", fn)
492 return False
493
494 mtime = bb.parse.cached_mtime_noerror(fn)
495
496 # Check file still exists
497 if mtime == 0:
498 logger.debug(2, "Cache: %s no longer exists", fn)
499 self.remove(fn)
500 return False
501
502 info_array = self.depends_cache[fn]
503 # Check the file's timestamp
504 if mtime != info_array[0].timestamp:
505 logger.debug(2, "Cache: %s changed", fn)
506 self.remove(fn)
507 return False
508
509 # Check dependencies are still valid
510 depends = info_array[0].file_depends
511 if depends:
512 for f, old_mtime in depends:
513 fmtime = bb.parse.cached_mtime_noerror(f)
514 # Check if file still exists
515 if old_mtime != 0 and fmtime == 0:
516 logger.debug(2, "Cache: %s's dependency %s was removed",
517 fn, f)
518 self.remove(fn)
519 return False
520
521 if (fmtime != old_mtime):
522 logger.debug(2, "Cache: %s's dependency %s changed",
523 fn, f)
524 self.remove(fn)
525 return False
526
527 if hasattr(info_array[0], 'file_checksums'):
528 for _, fl in info_array[0].file_checksums.items():
529 for f in fl.split():
530 if not os.path.exists(f):
531 logger.debug(2, "Cache: %s's file checksum list file %s was removed",
532 fn, f)
533 self.remove(fn)
534 return False
535
536 if appends != info_array[0].appends:
537 logger.debug(2, "Cache: appends for %s changed", fn)
538 logger.debug(2, "%s to %s" % (str(appends), str(info_array[0].appends)))
539 self.remove(fn)
540 return False
541
542 invalid = False
543 for cls in info_array[0].variants:
544 virtualfn = self.realfn2virtual(fn, cls)
545 self.clean.add(virtualfn)
546 if virtualfn not in self.depends_cache:
547 logger.debug(2, "Cache: %s is not cached", virtualfn)
548 invalid = True
549
550 # If any one of the variants is not present, mark as invalid for all
551 if invalid:
552 for cls in info_array[0].variants:
553 virtualfn = self.realfn2virtual(fn, cls)
554 if virtualfn in self.clean:
555 logger.debug(2, "Cache: Removing %s from cache", virtualfn)
556 self.clean.remove(virtualfn)
557 if fn in self.clean:
558 logger.debug(2, "Cache: Marking %s as not clean", fn)
559 self.clean.remove(fn)
560 return False
561
562 self.clean.add(fn)
563 return True
564
565 def remove(self, fn):
566 """
567 Remove a fn from the cache
568 Called from the parser in error cases
569 """
570 if fn in self.depends_cache:
571 logger.debug(1, "Removing %s from cache", fn)
572 del self.depends_cache[fn]
573 if fn in self.clean:
574 logger.debug(1, "Marking %s as unclean", fn)
575 self.clean.remove(fn)
576
577 def sync(self):
578 """
579 Save the cache
580 Called from the parser when complete (or exiting)
581 """
582
583 if not self.has_cache:
584 return
585
586 if self.cacheclean:
587 logger.debug(2, "Cache is clean, not saving.")
588 return
589
590 file_dict = {}
591 pickler_dict = {}
592 for cache_class in self.caches_array:
593 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
594 cache_class_name = cache_class.__name__
595 cachefile = getCacheFile(self.cachedir, cache_class.cachefile, self.data_hash)
596 file_dict[cache_class_name] = open(cachefile, "wb")
597 pickler_dict[cache_class_name] = pickle.Pickler(file_dict[cache_class_name], pickle.HIGHEST_PROTOCOL)
598
599 pickler_dict['CoreRecipeInfo'].dump(__cache_version__)
600 pickler_dict['CoreRecipeInfo'].dump(bb.__version__)
601
602 try:
603 for key, info_array in self.depends_cache.iteritems():
604 for info in info_array:
605 if isinstance(info, RecipeInfoCommon):
606 cache_class_name = info.__class__.__name__
607 pickler_dict[cache_class_name].dump(key)
608 pickler_dict[cache_class_name].dump(info)
609 finally:
610 for cache_class in self.caches_array:
611 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
612 cache_class_name = cache_class.__name__
613 file_dict[cache_class_name].close()
614
615 del self.depends_cache
616
617 @staticmethod
618 def mtime(cachefile):
619 return bb.parse.cached_mtime_noerror(cachefile)
620
621 def add_info(self, filename, info_array, cacheData, parsed=None):
622 if isinstance(info_array[0], CoreRecipeInfo) and (not info_array[0].skipped):
623 cacheData.add_from_recipeinfo(filename, info_array)
624
625 if not self.has_cache:
626 return
627
628 if (info_array[0].skipped or 'SRCREVINACTION' not in info_array[0].pv) and not info_array[0].nocache:
629 if parsed:
630 self.cacheclean = False
631 self.depends_cache[filename] = info_array
632
633 def add(self, file_name, data, cacheData, parsed=None):
634 """
635 Save data we need into the cache
636 """
637
638 realfn = self.virtualfn2realfn(file_name)[0]
639
640 info_array = []
641 for cache_class in self.caches_array:
642 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
643 info_array.append(cache_class(realfn, data))
644 self.add_info(file_name, info_array, cacheData, parsed)
645
646 @staticmethod
647 def load_bbfile(bbfile, appends, config):
648 """
649 Load and parse one .bb build file
650 Return the data and whether parsing resulted in the file being skipped
651 """
652 chdir_back = False
653
654 from bb import data, parse
655
656 # expand tmpdir to include this topdir
657 data.setVar('TMPDIR', data.getVar('TMPDIR', config, 1) or "", config)
658 bbfile_loc = os.path.abspath(os.path.dirname(bbfile))
659 oldpath = os.path.abspath(os.getcwd())
660 parse.cached_mtime_noerror(bbfile_loc)
661 bb_data = data.init_db(config)
662 # The ConfHandler first looks if there is a TOPDIR and if not
663 # then it would call getcwd().
664 # Previously, we chdir()ed to bbfile_loc, called the handler
665 # and finally chdir()ed back, a couple of thousand times. We now
666 # just fill in TOPDIR to point to bbfile_loc if there is no TOPDIR yet.
667 if not data.getVar('TOPDIR', bb_data):
668 chdir_back = True
669 data.setVar('TOPDIR', bbfile_loc, bb_data)
670 try:
671 if appends:
672 data.setVar('__BBAPPEND', " ".join(appends), bb_data)
673 bb_data = parse.handle(bbfile, bb_data)
674 if chdir_back:
675 os.chdir(oldpath)
676 return bb_data
677 except:
678 if chdir_back:
679 os.chdir(oldpath)
680 raise
681
682
683def init(cooker):
684 """
685 The Objective: Cache the minimum amount of data possible yet get to the
686 stage of building packages (i.e. tryBuild) without reparsing any .bb files.
687
688 To do this, we intercept getVar calls and only cache the variables we see
689 being accessed. We rely on the cache getVar calls being made for all
690 variables bitbake might need to use to reach this stage. For each cached
691 file we need to track:
692
693 * Its mtime
694 * The mtimes of all its dependencies
695 * Whether it caused a parse.SkipPackage exception
696
697 Files causing parsing errors are evicted from the cache.
698
699 """
700 return Cache(cooker.configuration.data, cooker.configuration.data_hash)
701
702
703class CacheData(object):
704 """
705 The data structures we compile from the cached data
706 """
707
708 def __init__(self, caches_array):
709 self.caches_array = caches_array
710 for cache_class in self.caches_array:
711 if type(cache_class) is type and issubclass(cache_class, RecipeInfoCommon):
712 cache_class.init_cacheData(self)
713
714 # Direct cache variables
715 self.task_queues = {}
716 self.preferred = {}
717 self.tasks = {}
718 # Indirect Cache variables (set elsewhere)
719 self.ignored_dependencies = []
720 self.world_target = set()
721 self.bbfile_priority = {}
722
723 def add_from_recipeinfo(self, fn, info_array):
724 for info in info_array:
725 info.add_cacheData(self, fn)
726
727class MultiProcessCache(object):
728 """
729 BitBake multi-process cache implementation
730
731 Used by the codeparser & file checksum caches
732 """
733
734 def __init__(self):
735 self.cachefile = None
736 self.cachedata = self.create_cachedata()
737 self.cachedata_extras = self.create_cachedata()
738
739 def init_cache(self, d):
740 cachedir = (d.getVar("PERSISTENT_DIR", True) or
741 d.getVar("CACHE", True))
742 if cachedir in [None, '']:
743 return
744 bb.utils.mkdirhier(cachedir)
745 self.cachefile = os.path.join(cachedir, self.__class__.cache_file_name)
746 logger.debug(1, "Using cache in '%s'", self.cachefile)
747
748 glf = bb.utils.lockfile(self.cachefile + ".lock")
749
750 try:
751 with open(self.cachefile, "rb") as f:
752 p = pickle.Unpickler(f)
753 data, version = p.load()
754 except:
755 bb.utils.unlockfile(glf)
756 return
757
758 bb.utils.unlockfile(glf)
759
760 if version != self.__class__.CACHE_VERSION:
761 return
762
763 self.cachedata = data
764
765 def internSet(self, items):
766 new = set()
767 for i in items:
768 new.add(intern(i))
769 return new
770
771 def compress_keys(self, data):
772 # Override in subclasses if desired
773 return
774
775 def create_cachedata(self):
776 data = [{}]
777 return data
778
779 def save_extras(self, d):
780 if not self.cachefile:
781 return
782
783 glf = bb.utils.lockfile(self.cachefile + ".lock", shared=True)
784
785 i = os.getpid()
786 lf = None
787 while not lf:
788 lf = bb.utils.lockfile(self.cachefile + ".lock." + str(i), retry=False)
789 if not lf or os.path.exists(self.cachefile + "-" + str(i)):
790 if lf:
791 bb.utils.unlockfile(lf)
792 lf = None
793 i = i + 1
794 continue
795
796 with open(self.cachefile + "-" + str(i), "wb") as f:
797 p = pickle.Pickler(f, -1)
798 p.dump([self.cachedata_extras, self.__class__.CACHE_VERSION])
799
800 bb.utils.unlockfile(lf)
801 bb.utils.unlockfile(glf)
802
803 def merge_data(self, source, dest):
804 for j in range(0,len(dest)):
805 for h in source[j]:
806 if h not in dest[j]:
807 dest[j][h] = source[j][h]
808
809 def save_merge(self, d):
810 if not self.cachefile:
811 return
812
813 glf = bb.utils.lockfile(self.cachefile + ".lock")
814
815 try:
816 with open(self.cachefile, "rb") as f:
817 p = pickle.Unpickler(f)
818 data, version = p.load()
819 except (IOError, EOFError):
820 data, version = None, None
821
822 if version != self.__class__.CACHE_VERSION:
823 data = self.create_cachedata()
824
825 for f in [y for y in os.listdir(os.path.dirname(self.cachefile)) if y.startswith(os.path.basename(self.cachefile) + '-')]:
826 f = os.path.join(os.path.dirname(self.cachefile), f)
827 try:
828 with open(f, "rb") as fd:
829 p = pickle.Unpickler(fd)
830 extradata, version = p.load()
831 except (IOError, EOFError):
832 extradata, version = self.create_cachedata(), None
833
834 if version != self.__class__.CACHE_VERSION:
835 continue
836
837 self.merge_data(extradata, data)
838 os.unlink(f)
839
840 self.compress_keys(data)
841
842 with open(self.cachefile, "wb") as f:
843 p = pickle.Pickler(f, -1)
844 p.dump([data, self.__class__.CACHE_VERSION])
845
846 bb.utils.unlockfile(glf)
847
diff --git a/bitbake/lib/bb/cache_extra.py b/bitbake/lib/bb/cache_extra.py
new file mode 100644
index 0000000000..83f4959d6c
--- /dev/null
+++ b/bitbake/lib/bb/cache_extra.py
@@ -0,0 +1,75 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# Extra RecipeInfo will be all defined in this file. Currently,
5# Only Hob (Image Creator) Requests some extra fields. So
6# HobRecipeInfo is defined. It's named HobRecipeInfo because it
7# is introduced by 'hob'. Users could also introduce other
8# RecipeInfo or simply use those already defined RecipeInfo.
9# In the following patch, this newly defined new extra RecipeInfo
10# will be dynamically loaded and used for loading/saving the extra
11# cache fields
12
13# Copyright (C) 2011, Intel Corporation. All rights reserved.
14
15# This program is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License version 2 as
17# published by the Free Software Foundation.
18#
19# This program is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License along
25# with this program; if not, write to the Free Software Foundation, Inc.,
26# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
27
28from bb.cache import RecipeInfoCommon
29
30class HobRecipeInfo(RecipeInfoCommon):
31 __slots__ = ()
32
33 classname = "HobRecipeInfo"
34 # please override this member with the correct data cache file
35 # such as (bb_cache.dat, bb_extracache_hob.dat)
36 cachefile = "bb_extracache_" + classname +".dat"
37
38 # override this member with the list of extra cache fields
39 # that this class will provide
40 cachefields = ['summary', 'license', 'section',
41 'description', 'homepage', 'bugtracker',
42 'prevision', 'files_info']
43
44 def __init__(self, filename, metadata):
45
46 self.summary = self.getvar('SUMMARY', metadata)
47 self.license = self.getvar('LICENSE', metadata)
48 self.section = self.getvar('SECTION', metadata)
49 self.description = self.getvar('DESCRIPTION', metadata)
50 self.homepage = self.getvar('HOMEPAGE', metadata)
51 self.bugtracker = self.getvar('BUGTRACKER', metadata)
52 self.prevision = self.getvar('PR', metadata)
53 self.files_info = self.getvar('FILES_INFO', metadata)
54
55 @classmethod
56 def init_cacheData(cls, cachedata):
57 # CacheData in Hob RecipeInfo Class
58 cachedata.summary = {}
59 cachedata.license = {}
60 cachedata.section = {}
61 cachedata.description = {}
62 cachedata.homepage = {}
63 cachedata.bugtracker = {}
64 cachedata.prevision = {}
65 cachedata.files_info = {}
66
67 def add_cacheData(self, cachedata, fn):
68 cachedata.summary[fn] = self.summary
69 cachedata.license[fn] = self.license
70 cachedata.section[fn] = self.section
71 cachedata.description[fn] = self.description
72 cachedata.homepage[fn] = self.homepage
73 cachedata.bugtracker[fn] = self.bugtracker
74 cachedata.prevision[fn] = self.prevision
75 cachedata.files_info[fn] = self.files_info
diff --git a/bitbake/lib/bb/checksum.py b/bitbake/lib/bb/checksum.py
new file mode 100644
index 0000000000..514ff0b1e6
--- /dev/null
+++ b/bitbake/lib/bb/checksum.py
@@ -0,0 +1,90 @@
1# Local file checksum cache implementation
2#
3# Copyright (C) 2012 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 os
19import stat
20import bb.utils
21import logging
22from bb.cache import MultiProcessCache
23
24logger = logging.getLogger("BitBake.Cache")
25
26try:
27 import cPickle as pickle
28except ImportError:
29 import pickle
30 logger.info("Importing cPickle failed. "
31 "Falling back to a very slow implementation.")
32
33
34# mtime cache (non-persistent)
35# based upon the assumption that files do not change during bitbake run
36class FileMtimeCache(object):
37 cache = {}
38
39 def cached_mtime(self, f):
40 if f not in self.cache:
41 self.cache[f] = os.stat(f)[stat.ST_MTIME]
42 return self.cache[f]
43
44 def cached_mtime_noerror(self, f):
45 if f not in self.cache:
46 try:
47 self.cache[f] = os.stat(f)[stat.ST_MTIME]
48 except OSError:
49 return 0
50 return self.cache[f]
51
52 def update_mtime(self, f):
53 self.cache[f] = os.stat(f)[stat.ST_MTIME]
54 return self.cache[f]
55
56 def clear(self):
57 self.cache.clear()
58
59# Checksum + mtime cache (persistent)
60class FileChecksumCache(MultiProcessCache):
61 cache_file_name = "local_file_checksum_cache.dat"
62 CACHE_VERSION = 1
63
64 def __init__(self):
65 self.mtime_cache = FileMtimeCache()
66 MultiProcessCache.__init__(self)
67
68 def get_checksum(self, f):
69 entry = self.cachedata[0].get(f)
70 cmtime = self.mtime_cache.cached_mtime(f)
71 if entry:
72 (mtime, hashval) = entry
73 if cmtime == mtime:
74 return hashval
75 else:
76 bb.debug(2, "file %s changed mtime, recompute checksum" % f)
77
78 hashval = bb.utils.md5_file(f)
79 self.cachedata_extras[0][f] = (cmtime, hashval)
80 return hashval
81
82 def merge_data(self, source, dest):
83 for h in source[0]:
84 if h in dest:
85 (smtime, _) = source[0][h]
86 (dmtime, _) = dest[0][h]
87 if smtime > dmtime:
88 dest[0][h] = source[0][h]
89 else:
90 dest[0][h] = source[0][h]
diff --git a/bitbake/lib/bb/codeparser.py b/bitbake/lib/bb/codeparser.py
new file mode 100644
index 0000000000..e44e791585
--- /dev/null
+++ b/bitbake/lib/bb/codeparser.py
@@ -0,0 +1,319 @@
1import ast
2import codegen
3import logging
4import os.path
5import bb.utils, bb.data
6from itertools import chain
7from pysh import pyshyacc, pyshlex, sherrors
8from bb.cache import MultiProcessCache
9
10
11logger = logging.getLogger('BitBake.CodeParser')
12
13try:
14 import cPickle as pickle
15except ImportError:
16 import pickle
17 logger.info('Importing cPickle failed. Falling back to a very slow implementation.')
18
19
20def check_indent(codestr):
21 """If the code is indented, add a top level piece of code to 'remove' the indentation"""
22
23 i = 0
24 while codestr[i] in ["\n", "\t", " "]:
25 i = i + 1
26
27 if i == 0:
28 return codestr
29
30 if codestr[i-1] == "\t" or codestr[i-1] == " ":
31 return "if 1:\n" + codestr
32
33 return codestr
34
35
36class CodeParserCache(MultiProcessCache):
37 cache_file_name = "bb_codeparser.dat"
38 CACHE_VERSION = 3
39
40 def __init__(self):
41 MultiProcessCache.__init__(self)
42 self.pythoncache = self.cachedata[0]
43 self.shellcache = self.cachedata[1]
44 self.pythoncacheextras = self.cachedata_extras[0]
45 self.shellcacheextras = self.cachedata_extras[1]
46
47 def init_cache(self, d):
48 MultiProcessCache.init_cache(self, d)
49
50 # cachedata gets re-assigned in the parent
51 self.pythoncache = self.cachedata[0]
52 self.shellcache = self.cachedata[1]
53
54 def compress_keys(self, data):
55 # When the dicts are originally created, python calls intern() on the set keys
56 # which significantly improves memory usage. Sadly the pickle/unpickle process
57 # doesn't call intern() on the keys and results in the same strings being duplicated
58 # in memory. This also means pickle will save the same string multiple times in
59 # the cache file. By interning the data here, the cache file shrinks dramatically
60 # meaning faster load times and the reloaded cache files also consume much less
61 # memory. This is worth any performance hit from this loops and the use of the
62 # intern() data storage.
63 # Python 3.x may behave better in this area
64 for h in data[0]:
65 data[0][h]["refs"] = self.internSet(data[0][h]["refs"])
66 data[0][h]["execs"] = self.internSet(data[0][h]["execs"])
67 for h in data[1]:
68 data[1][h]["execs"] = self.internSet(data[1][h]["execs"])
69 return
70
71 def create_cachedata(self):
72 data = [{}, {}]
73 return data
74
75codeparsercache = CodeParserCache()
76
77def parser_cache_init(d):
78 codeparsercache.init_cache(d)
79
80def parser_cache_save(d):
81 codeparsercache.save_extras(d)
82
83def parser_cache_savemerge(d):
84 codeparsercache.save_merge(d)
85
86Logger = logging.getLoggerClass()
87class BufferedLogger(Logger):
88 def __init__(self, name, level=0, target=None):
89 Logger.__init__(self, name)
90 self.setLevel(level)
91 self.buffer = []
92 self.target = target
93
94 def handle(self, record):
95 self.buffer.append(record)
96
97 def flush(self):
98 for record in self.buffer:
99 self.target.handle(record)
100 self.buffer = []
101
102class PythonParser():
103 getvars = ("d.getVar", "bb.data.getVar", "data.getVar", "d.appendVar", "d.prependVar")
104 containsfuncs = ("bb.utils.contains", "base_contains", "oe.utils.contains")
105 execfuncs = ("bb.build.exec_func", "bb.build.exec_task")
106
107 def warn(self, func, arg):
108 """Warn about calls of bitbake APIs which pass a non-literal
109 argument for the variable name, as we're not able to track such
110 a reference.
111 """
112
113 try:
114 funcstr = codegen.to_source(func)
115 argstr = codegen.to_source(arg)
116 except TypeError:
117 self.log.debug(2, 'Failed to convert function and argument to source form')
118 else:
119 self.log.debug(1, self.unhandled_message % (funcstr, argstr))
120
121 def visit_Call(self, node):
122 name = self.called_node_name(node.func)
123 if name in self.getvars or name in self.containsfuncs:
124 if isinstance(node.args[0], ast.Str):
125 self.var_references.add(node.args[0].s)
126 else:
127 self.warn(node.func, node.args[0])
128 elif name in self.execfuncs:
129 if isinstance(node.args[0], ast.Str):
130 self.var_execs.add(node.args[0].s)
131 else:
132 self.warn(node.func, node.args[0])
133 elif name and isinstance(node.func, (ast.Name, ast.Attribute)):
134 self.execs.add(name)
135
136 def called_node_name(self, node):
137 """Given a called node, return its original string form"""
138 components = []
139 while node:
140 if isinstance(node, ast.Attribute):
141 components.append(node.attr)
142 node = node.value
143 elif isinstance(node, ast.Name):
144 components.append(node.id)
145 return '.'.join(reversed(components))
146 else:
147 break
148
149 def __init__(self, name, log):
150 self.var_references = set()
151 self.var_execs = set()
152 self.execs = set()
153 self.references = set()
154 self.log = BufferedLogger('BitBake.Data.%s' % name, logging.DEBUG, log)
155
156 self.unhandled_message = "in call of %s, argument '%s' is not a string literal"
157 self.unhandled_message = "while parsing %s, %s" % (name, self.unhandled_message)
158
159 def parse_python(self, node):
160 h = hash(str(node))
161
162 if h in codeparsercache.pythoncache:
163 self.references = codeparsercache.pythoncache[h]["refs"]
164 self.execs = codeparsercache.pythoncache[h]["execs"]
165 return
166
167 if h in codeparsercache.pythoncacheextras:
168 self.references = codeparsercache.pythoncacheextras[h]["refs"]
169 self.execs = codeparsercache.pythoncacheextras[h]["execs"]
170 return
171
172
173 code = compile(check_indent(str(node)), "<string>", "exec",
174 ast.PyCF_ONLY_AST)
175
176 for n in ast.walk(code):
177 if n.__class__.__name__ == "Call":
178 self.visit_Call(n)
179
180 self.references.update(self.var_references)
181 self.references.update(self.var_execs)
182
183 codeparsercache.pythoncacheextras[h] = {}
184 codeparsercache.pythoncacheextras[h]["refs"] = self.references
185 codeparsercache.pythoncacheextras[h]["execs"] = self.execs
186
187class ShellParser():
188 def __init__(self, name, log):
189 self.funcdefs = set()
190 self.allexecs = set()
191 self.execs = set()
192 self.log = BufferedLogger('BitBake.Data.%s' % name, logging.DEBUG, log)
193 self.unhandled_template = "unable to handle non-literal command '%s'"
194 self.unhandled_template = "while parsing %s, %s" % (name, self.unhandled_template)
195
196 def parse_shell(self, value):
197 """Parse the supplied shell code in a string, returning the external
198 commands it executes.
199 """
200
201 h = hash(str(value))
202
203 if h in codeparsercache.shellcache:
204 self.execs = codeparsercache.shellcache[h]["execs"]
205 return self.execs
206
207 if h in codeparsercache.shellcacheextras:
208 self.execs = codeparsercache.shellcacheextras[h]["execs"]
209 return self.execs
210
211 try:
212 tokens, _ = pyshyacc.parse(value, eof=True, debug=False)
213 except pyshlex.NeedMore:
214 raise sherrors.ShellSyntaxError("Unexpected EOF")
215
216 for token in tokens:
217 self.process_tokens(token)
218 self.execs = set(cmd for cmd in self.allexecs if cmd not in self.funcdefs)
219
220 codeparsercache.shellcacheextras[h] = {}
221 codeparsercache.shellcacheextras[h]["execs"] = self.execs
222
223 return self.execs
224
225 def process_tokens(self, tokens):
226 """Process a supplied portion of the syntax tree as returned by
227 pyshyacc.parse.
228 """
229
230 def function_definition(value):
231 self.funcdefs.add(value.name)
232 return [value.body], None
233
234 def case_clause(value):
235 # Element 0 of each item in the case is the list of patterns, and
236 # Element 1 of each item in the case is the list of commands to be
237 # executed when that pattern matches.
238 words = chain(*[item[0] for item in value.items])
239 cmds = chain(*[item[1] for item in value.items])
240 return cmds, words
241
242 def if_clause(value):
243 main = chain(value.cond, value.if_cmds)
244 rest = value.else_cmds
245 if isinstance(rest, tuple) and rest[0] == "elif":
246 return chain(main, if_clause(rest[1]))
247 else:
248 return chain(main, rest)
249
250 def simple_command(value):
251 return None, chain(value.words, (assign[1] for assign in value.assigns))
252
253 token_handlers = {
254 "and_or": lambda x: ((x.left, x.right), None),
255 "async": lambda x: ([x], None),
256 "brace_group": lambda x: (x.cmds, None),
257 "for_clause": lambda x: (x.cmds, x.items),
258 "function_definition": function_definition,
259 "if_clause": lambda x: (if_clause(x), None),
260 "pipeline": lambda x: (x.commands, None),
261 "redirect_list": lambda x: ([x.cmd], None),
262 "subshell": lambda x: (x.cmds, None),
263 "while_clause": lambda x: (chain(x.condition, x.cmds), None),
264 "until_clause": lambda x: (chain(x.condition, x.cmds), None),
265 "simple_command": simple_command,
266 "case_clause": case_clause,
267 }
268
269 for token in tokens:
270 name, value = token
271 try:
272 more_tokens, words = token_handlers[name](value)
273 except KeyError:
274 raise NotImplementedError("Unsupported token type " + name)
275
276 if more_tokens:
277 self.process_tokens(more_tokens)
278
279 if words:
280 self.process_words(words)
281
282 def process_words(self, words):
283 """Process a set of 'words' in pyshyacc parlance, which includes
284 extraction of executed commands from $() blocks, as well as grabbing
285 the command name argument.
286 """
287
288 words = list(words)
289 for word in list(words):
290 wtree = pyshlex.make_wordtree(word[1])
291 for part in wtree:
292 if not isinstance(part, list):
293 continue
294
295 if part[0] in ('`', '$('):
296 command = pyshlex.wordtree_as_string(part[1:-1])
297 self.parse_shell(command)
298
299 if word[0] in ("cmd_name", "cmd_word"):
300 if word in words:
301 words.remove(word)
302
303 usetoken = False
304 for word in words:
305 if word[0] in ("cmd_name", "cmd_word") or \
306 (usetoken and word[0] == "TOKEN"):
307 if "=" in word[1]:
308 usetoken = True
309 continue
310
311 cmd = word[1]
312 if cmd.startswith("$"):
313 self.log.debug(1, self.unhandled_template % cmd)
314 elif cmd == "eval":
315 command = " ".join(word for _, word in words[1:])
316 self.parse_shell(command)
317 else:
318 self.allexecs.add(cmd)
319 break
diff --git a/bitbake/lib/bb/command.py b/bitbake/lib/bb/command.py
new file mode 100644
index 0000000000..3ca27a69e0
--- /dev/null
+++ b/bitbake/lib/bb/command.py
@@ -0,0 +1,441 @@
1"""
2BitBake 'Command' module
3
4Provide an interface to interact with the bitbake server through 'commands'
5"""
6
7# Copyright (C) 2006-2007 Richard Purdie
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22"""
23The bitbake server takes 'commands' from its UI/commandline.
24Commands are either synchronous or asynchronous.
25Async commands return data to the client in the form of events.
26Sync commands must only return data through the function return value
27and must not trigger events, directly or indirectly.
28Commands are queued in a CommandQueue
29"""
30
31import bb.event
32import bb.cooker
33
34class CommandCompleted(bb.event.Event):
35 pass
36
37class CommandExit(bb.event.Event):
38 def __init__(self, exitcode):
39 bb.event.Event.__init__(self)
40 self.exitcode = int(exitcode)
41
42class CommandFailed(CommandExit):
43 def __init__(self, message):
44 self.error = message
45 CommandExit.__init__(self, 1)
46
47class CommandError(Exception):
48 pass
49
50class Command:
51 """
52 A queue of asynchronous commands for bitbake
53 """
54 def __init__(self, cooker):
55 self.cooker = cooker
56 self.cmds_sync = CommandsSync()
57 self.cmds_async = CommandsAsync()
58
59 # FIXME Add lock for this
60 self.currentAsyncCommand = None
61
62 def runCommand(self, commandline, ro_only = False):
63 command = commandline.pop(0)
64 if hasattr(CommandsSync, command):
65 # Can run synchronous commands straight away
66 command_method = getattr(self.cmds_sync, command)
67 if ro_only:
68 if not hasattr(command_method, 'readonly') or False == getattr(command_method, 'readonly'):
69 return None, "Not able to execute not readonly commands in readonly mode"
70 try:
71 result = command_method(self, commandline)
72 except CommandError as exc:
73 return None, exc.args[0]
74 except Exception:
75 import traceback
76 return None, traceback.format_exc()
77 else:
78 return result, None
79 if self.currentAsyncCommand is not None:
80 return None, "Busy (%s in progress)" % self.currentAsyncCommand[0]
81 if command not in CommandsAsync.__dict__:
82 return None, "No such command"
83 self.currentAsyncCommand = (command, commandline)
84 self.cooker.configuration.server_register_idlecallback(self.cooker.runCommands, self.cooker)
85 return True, None
86
87 def runAsyncCommand(self):
88 try:
89 if self.currentAsyncCommand is not None:
90 (command, options) = self.currentAsyncCommand
91 commandmethod = getattr(CommandsAsync, command)
92 needcache = getattr( commandmethod, "needcache" )
93 if needcache and self.cooker.state != bb.cooker.state.running:
94 self.cooker.updateCache()
95 return True
96 else:
97 commandmethod(self.cmds_async, self, options)
98 return False
99 else:
100 return False
101 except KeyboardInterrupt as exc:
102 self.finishAsyncCommand("Interrupted")
103 return False
104 except SystemExit as exc:
105 arg = exc.args[0]
106 if isinstance(arg, basestring):
107 self.finishAsyncCommand(arg)
108 else:
109 self.finishAsyncCommand("Exited with %s" % arg)
110 return False
111 except Exception as exc:
112 import traceback
113 if isinstance(exc, bb.BBHandledException):
114 self.finishAsyncCommand("")
115 else:
116 self.finishAsyncCommand(traceback.format_exc())
117 return False
118
119 def finishAsyncCommand(self, msg=None, code=None):
120 if msg or msg == "":
121 bb.event.fire(CommandFailed(msg), self.cooker.event_data)
122 elif code:
123 bb.event.fire(CommandExit(code), self.cooker.event_data)
124 else:
125 bb.event.fire(CommandCompleted(), self.cooker.event_data)
126 self.currentAsyncCommand = None
127 self.cooker.finishcommand()
128
129class CommandsSync:
130 """
131 A class of synchronous commands
132 These should run quickly so as not to hurt interactive performance.
133 These must not influence any running synchronous command.
134 """
135
136 def stateShutdown(self, command, params):
137 """
138 Trigger cooker 'shutdown' mode
139 """
140 command.cooker.shutdown(False)
141
142 def stateForceShutdown(self, command, params):
143 """
144 Stop the cooker
145 """
146 command.cooker.shutdown(True)
147
148 def getAllKeysWithFlags(self, command, params):
149 """
150 Returns a dump of the global state. Call with
151 variable flags to be retrieved as params.
152 """
153 flaglist = params[0]
154 return command.cooker.getAllKeysWithFlags(flaglist)
155 getAllKeysWithFlags.readonly = True
156
157 def getVariable(self, command, params):
158 """
159 Read the value of a variable from data
160 """
161 varname = params[0]
162 expand = True
163 if len(params) > 1:
164 expand = (params[1] == "True")
165
166 return command.cooker.data.getVar(varname, expand)
167 getVariable.readonly = True
168
169 def setVariable(self, command, params):
170 """
171 Set the value of variable in data
172 """
173 varname = params[0]
174 value = str(params[1])
175 command.cooker.data.setVar(varname, value)
176
177 def setConfig(self, command, params):
178 """
179 Set the value of variable in configuration
180 """
181 varname = params[0]
182 value = str(params[1])
183 setattr(command.cooker.configuration, varname, value)
184
185 def enableDataTracking(self, command, params):
186 """
187 Enable history tracking for variables
188 """
189 command.cooker.enableDataTracking()
190
191 def disableDataTracking(self, command, params):
192 """
193 Disable history tracking for variables
194 """
195 command.cooker.disableDataTracking()
196
197 def initCooker(self, command, params):
198 """
199 Init the cooker to initial state with nothing parsed
200 """
201 command.cooker.initialize()
202
203 def resetCooker(self, command, params):
204 """
205 Reset the cooker to its initial state, thus forcing a reparse for
206 any async command that has the needcache property set to True
207 """
208 command.cooker.reset()
209
210 def getCpuCount(self, command, params):
211 """
212 Get the CPU count on the bitbake server
213 """
214 return bb.utils.cpu_count()
215 getCpuCount.readonly = True
216
217 def matchFile(self, command, params):
218 fMatch = params[0]
219 return command.cooker.matchFile(fMatch)
220
221 def generateNewImage(self, command, params):
222 image = params[0]
223 base_image = params[1]
224 package_queue = params[2]
225 timestamp = params[3]
226 description = params[4]
227 return command.cooker.generateNewImage(image, base_image,
228 package_queue, timestamp, description)
229
230 def ensureDir(self, command, params):
231 directory = params[0]
232 bb.utils.mkdirhier(directory)
233
234 def setVarFile(self, command, params):
235 """
236 Save a variable in a file; used for saving in a configuration file
237 """
238 var = params[0]
239 val = params[1]
240 default_file = params[2]
241 op = params[3]
242 command.cooker.modifyConfigurationVar(var, val, default_file, op)
243
244 def removeVarFile(self, command, params):
245 """
246 Remove a variable declaration from a file
247 """
248 var = params[0]
249 command.cooker.removeConfigurationVar(var)
250
251 def createConfigFile(self, command, params):
252 """
253 Create an extra configuration file
254 """
255 name = params[0]
256 command.cooker.createConfigFile(name)
257
258 def setEventMask(self, command, params):
259 handlerNum = params[0]
260 llevel = params[1]
261 debug_domains = params[2]
262 mask = params[3]
263 return bb.event.set_UIHmask(handlerNum, llevel, debug_domains, mask)
264
265class CommandsAsync:
266 """
267 A class of asynchronous commands
268 These functions communicate via generated events.
269 Any function that requires metadata parsing should be here.
270 """
271
272 def buildFile(self, command, params):
273 """
274 Build a single specified .bb file
275 """
276 bfile = params[0]
277 task = params[1]
278
279 command.cooker.buildFile(bfile, task)
280 buildFile.needcache = False
281
282 def buildTargets(self, command, params):
283 """
284 Build a set of targets
285 """
286 pkgs_to_build = params[0]
287 task = params[1]
288
289 command.cooker.buildTargets(pkgs_to_build, task)
290 buildTargets.needcache = True
291
292 def generateDepTreeEvent(self, command, params):
293 """
294 Generate an event containing the dependency information
295 """
296 pkgs_to_build = params[0]
297 task = params[1]
298
299 command.cooker.generateDepTreeEvent(pkgs_to_build, task)
300 command.finishAsyncCommand()
301 generateDepTreeEvent.needcache = True
302
303 def generateDotGraph(self, command, params):
304 """
305 Dump dependency information to disk as .dot files
306 """
307 pkgs_to_build = params[0]
308 task = params[1]
309
310 command.cooker.generateDotGraphFiles(pkgs_to_build, task)
311 command.finishAsyncCommand()
312 generateDotGraph.needcache = True
313
314 def generateTargetsTree(self, command, params):
315 """
316 Generate a tree of buildable targets.
317 If klass is provided ensure all recipes that inherit the class are
318 included in the package list.
319 If pkg_list provided use that list (plus any extras brought in by
320 klass) rather than generating a tree for all packages.
321 """
322 klass = params[0]
323 pkg_list = params[1]
324
325 command.cooker.generateTargetsTree(klass, pkg_list)
326 command.finishAsyncCommand()
327 generateTargetsTree.needcache = True
328
329 def findCoreBaseFiles(self, command, params):
330 """
331 Find certain files in COREBASE directory. i.e. Layers
332 """
333 subdir = params[0]
334 filename = params[1]
335
336 command.cooker.findCoreBaseFiles(subdir, filename)
337 command.finishAsyncCommand()
338 findCoreBaseFiles.needcache = False
339
340 def findConfigFiles(self, command, params):
341 """
342 Find config files which provide appropriate values
343 for the passed configuration variable. i.e. MACHINE
344 """
345 varname = params[0]
346
347 command.cooker.findConfigFiles(varname)
348 command.finishAsyncCommand()
349 findConfigFiles.needcache = False
350
351 def findFilesMatchingInDir(self, command, params):
352 """
353 Find implementation files matching the specified pattern
354 in the requested subdirectory of a BBPATH
355 """
356 pattern = params[0]
357 directory = params[1]
358
359 command.cooker.findFilesMatchingInDir(pattern, directory)
360 command.finishAsyncCommand()
361 findFilesMatchingInDir.needcache = False
362
363 def findConfigFilePath(self, command, params):
364 """
365 Find the path of the requested configuration file
366 """
367 configfile = params[0]
368
369 command.cooker.findConfigFilePath(configfile)
370 command.finishAsyncCommand()
371 findConfigFilePath.needcache = False
372
373 def showVersions(self, command, params):
374 """
375 Show the currently selected versions
376 """
377 command.cooker.showVersions()
378 command.finishAsyncCommand()
379 showVersions.needcache = True
380
381 def showEnvironmentTarget(self, command, params):
382 """
383 Print the environment of a target recipe
384 (needs the cache to work out which recipe to use)
385 """
386 pkg = params[0]
387
388 command.cooker.showEnvironment(None, pkg)
389 command.finishAsyncCommand()
390 showEnvironmentTarget.needcache = True
391
392 def showEnvironment(self, command, params):
393 """
394 Print the standard environment
395 or if specified the environment for a specified recipe
396 """
397 bfile = params[0]
398
399 command.cooker.showEnvironment(bfile)
400 command.finishAsyncCommand()
401 showEnvironment.needcache = False
402
403 def parseFiles(self, command, params):
404 """
405 Parse the .bb files
406 """
407 command.cooker.updateCache()
408 command.finishAsyncCommand()
409 parseFiles.needcache = True
410
411 def compareRevisions(self, command, params):
412 """
413 Parse the .bb files
414 """
415 if bb.fetch.fetcher_compare_revisions(command.cooker.data):
416 command.finishAsyncCommand(code=1)
417 else:
418 command.finishAsyncCommand()
419 compareRevisions.needcache = True
420
421 def parseConfigurationFiles(self, command, params):
422 """
423 Parse the configuration files
424 """
425 prefiles = params[0].split()
426 postfiles = params[1].split()
427 command.cooker.configuration.prefile = prefiles
428 command.cooker.configuration.postfile = postfiles
429 command.cooker.loadConfigurationData()
430 command.finishAsyncCommand()
431 parseConfigurationFiles.needcache = False
432
433 def triggerEvent(self, command, params):
434 """
435 Trigger a certain event
436 """
437 event = params[0]
438 bb.event.fire(eval(event), command.cooker.data)
439 command.currentAsyncCommand = None
440 triggerEvent.needcache = False
441
diff --git a/bitbake/lib/bb/compat.py b/bitbake/lib/bb/compat.py
new file mode 100644
index 0000000000..de1923d28a
--- /dev/null
+++ b/bitbake/lib/bb/compat.py
@@ -0,0 +1,6 @@
1"""Code pulled from future python versions, here for compatibility"""
2
3from collections import MutableMapping, KeysView, ValuesView, ItemsView, OrderedDict
4from functools import total_ordering
5
6
diff --git a/bitbake/lib/bb/cooker.py b/bitbake/lib/bb/cooker.py
new file mode 100644
index 0000000000..ad36b34aa4
--- /dev/null
+++ b/bitbake/lib/bb/cooker.py
@@ -0,0 +1,1838 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# Copyright (C) 2003, 2004 Chris Larson
6# Copyright (C) 2003, 2004 Phil Blundell
7# Copyright (C) 2003 - 2005 Michael 'Mickey' Lauer
8# Copyright (C) 2005 Holger Hans Peter Freyther
9# Copyright (C) 2005 ROAD GmbH
10# Copyright (C) 2006 - 2007 Richard Purdie
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License version 2 as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along
22# with this program; if not, write to the Free Software Foundation, Inc.,
23# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24
25from __future__ import print_function
26import sys, os, glob, os.path, re, time
27import atexit
28import itertools
29import logging
30import multiprocessing
31import sre_constants
32import threading
33from cStringIO import StringIO
34from contextlib import closing
35from functools import wraps
36from collections import defaultdict
37import bb, bb.exceptions, bb.command
38from bb import utils, data, parse, event, cache, providers, taskdata, runqueue
39import Queue
40import prserv.serv
41
42logger = logging.getLogger("BitBake")
43collectlog = logging.getLogger("BitBake.Collection")
44buildlog = logging.getLogger("BitBake.Build")
45parselog = logging.getLogger("BitBake.Parsing")
46providerlog = logging.getLogger("BitBake.Provider")
47
48class NoSpecificMatch(bb.BBHandledException):
49 """
50 Exception raised when no or multiple file matches are found
51 """
52
53class NothingToBuild(Exception):
54 """
55 Exception raised when there is nothing to build
56 """
57
58class CollectionError(bb.BBHandledException):
59 """
60 Exception raised when layer configuration is incorrect
61 """
62
63class state:
64 initial, parsing, running, shutdown, forceshutdown, stopped = range(6)
65
66
67class SkippedPackage:
68 def __init__(self, info = None, reason = None):
69 self.pn = None
70 self.skipreason = None
71 self.provides = None
72 self.rprovides = None
73
74 if info:
75 self.pn = info.pn
76 self.skipreason = info.skipreason
77 self.provides = info.provides
78 self.rprovides = info.rprovides
79 elif reason:
80 self.skipreason = reason
81
82
83class CookerFeatures(object):
84 _feature_list = [HOB_EXTRA_CACHES, SEND_DEPENDS_TREE] = range(2)
85
86 def __init__(self):
87 self._features=set()
88
89 def setFeature(self, f):
90 # validate we got a request for a feature we support
91 if f not in CookerFeatures._feature_list:
92 return
93 self._features.add(f)
94
95 def __contains__(self, f):
96 return f in self._features
97
98 def __iter__(self):
99 return self._features.__iter__()
100
101 def next(self):
102 return self._features.next()
103
104
105#============================================================================#
106# BBCooker
107#============================================================================#
108class BBCooker:
109 """
110 Manages one bitbake build run
111 """
112
113 def __init__(self, configuration):
114 self.recipecache = None
115 self.skiplist = {}
116 self.featureset = CookerFeatures()
117
118 self.configuration = configuration
119
120 self.loadConfigurationData()
121
122 # Take a lock so only one copy of bitbake can run against a given build
123 # directory at a time
124 lockfile = self.data.expand("${TOPDIR}/bitbake.lock")
125 self.lock = bb.utils.lockfile(lockfile, False, False)
126 if not self.lock:
127 bb.fatal("Only one copy of bitbake should be run against a build directory")
128
129 # TOSTOP must not be set or our children will hang when they output
130 fd = sys.stdout.fileno()
131 if os.isatty(fd):
132 import termios
133 tcattr = termios.tcgetattr(fd)
134 if tcattr[3] & termios.TOSTOP:
135 buildlog.info("The terminal had the TOSTOP bit set, clearing...")
136 tcattr[3] = tcattr[3] & ~termios.TOSTOP
137 termios.tcsetattr(fd, termios.TCSANOW, tcattr)
138
139 self.command = bb.command.Command(self)
140 self.state = state.initial
141
142 self.parser = None
143
144 def initConfigurationData(self):
145
146 self.state = state.initial
147
148 self.caches_array = []
149
150 all_extra_cache_names = []
151 # We hardcode all known cache types in a single place, here.
152 if CookerFeatures.HOB_EXTRA_CACHES in self.featureset:
153 all_extra_cache_names.append("bb.cache_extra:HobRecipeInfo")
154
155 caches_name_array = ['bb.cache:CoreRecipeInfo'] + all_extra_cache_names
156
157 # At least CoreRecipeInfo will be loaded, so caches_array will never be empty!
158 # This is the entry point, no further check needed!
159 for var in caches_name_array:
160 try:
161 module_name, cache_name = var.split(':')
162 module = __import__(module_name, fromlist=(cache_name,))
163 self.caches_array.append(getattr(module, cache_name))
164 except ImportError as exc:
165 logger.critical("Unable to import extra RecipeInfo '%s' from '%s': %s" % (cache_name, module_name, exc))
166 sys.exit("FATAL: Failed to import extra cache class '%s'." % cache_name)
167
168 self.databuilder = bb.cookerdata.CookerDataBuilder(self.configuration, False)
169 self.data = self.databuilder.data
170
171 def enableDataTracking(self):
172 self.configuration.tracking = True
173 self.data.enableTracking()
174
175 def disableDataTracking(self):
176 self.configuration.tracking = False
177 self.data.disableTracking()
178
179 def loadConfigurationData(self):
180 self.initConfigurationData()
181 self.databuilder.parseBaseConfiguration()
182 self.data = self.databuilder.data
183 self.data_hash = self.databuilder.data_hash
184
185 #
186 # Special updated configuration we use for firing events
187 #
188 self.event_data = bb.data.createCopy(self.data)
189 bb.data.update_data(self.event_data)
190 bb.parse.init_parser(self.event_data)
191
192 def modifyConfigurationVar(self, var, val, default_file, op):
193 if op == "append":
194 self.appendConfigurationVar(var, val, default_file)
195 elif op == "set":
196 self.saveConfigurationVar(var, val, default_file, "=")
197 elif op == "earlyAssign":
198 self.saveConfigurationVar(var, val, default_file, "?=")
199
200
201 def appendConfigurationVar(self, var, val, default_file):
202 #add append var operation to the end of default_file
203 default_file = bb.cookerdata.findConfigFile(default_file, self.data)
204
205 with open(default_file, 'r') as f:
206 contents = f.readlines()
207 f.close()
208
209 total = ""
210 for c in contents:
211 total += c
212
213 total += "#added by hob"
214 total += "\n%s += \"%s\"\n" % (var, val)
215
216 with open(default_file, 'w') as f:
217 f.write(total)
218 f.close()
219
220 #add to history
221 loginfo = {"op":append, "file":default_file, "line":total.count("\n")}
222 self.data.appendVar(var, val, **loginfo)
223
224 def saveConfigurationVar(self, var, val, default_file, op):
225
226 replaced = False
227 #do not save if nothing changed
228 if str(val) == self.data.getVar(var):
229 return
230
231 conf_files = self.data.varhistory.get_variable_files(var)
232
233 #format the value when it is a list
234 if isinstance(val, list):
235 listval = ""
236 for value in val:
237 listval += "%s " % value
238 val = listval
239
240 topdir = self.data.getVar("TOPDIR")
241
242 #comment or replace operations made on var
243 for conf_file in conf_files:
244 if topdir in conf_file:
245 with open(conf_file, 'r') as f:
246 contents = f.readlines()
247 f.close()
248
249 lines = self.data.varhistory.get_variable_lines(var, conf_file)
250 for line in lines:
251 total = ""
252 i = 0
253 for c in contents:
254 total += c
255 i = i + 1
256 if i==int(line):
257 end_index = len(total)
258 index = total.rfind(var, 0, end_index)
259
260 begin_line = total.count("\n",0,index)
261 end_line = int(line)
262
263 #check if the variable was saved before in the same way
264 #if true it replace the place where the variable was declared
265 #else it comments it
266 if contents[begin_line-1]== "#added by hob\n":
267 contents[begin_line] = "%s %s \"%s\"\n" % (var, op, val)
268 replaced = True
269 else:
270 for ii in range(begin_line, end_line):
271 contents[ii] = "#" + contents[ii]
272
273 total = ""
274 for c in contents:
275 total += c
276 with open(conf_file, 'w') as f:
277 f.write(total)
278 f.close()
279
280 if replaced == False:
281 #remove var from history
282 self.data.varhistory.del_var_history(var)
283
284 #add var to the end of default_file
285 default_file = bb.cookerdata.findConfigFile(default_file, self.data)
286
287 with open(default_file, 'r') as f:
288 contents = f.readlines()
289 f.close()
290
291 total = ""
292 for c in contents:
293 total += c
294
295 #add the variable on a single line, to be easy to replace the second time
296 total += "\n#added by hob"
297 total += "\n%s %s \"%s\"\n" % (var, op, val)
298
299 with open(default_file, 'w') as f:
300 f.write(total)
301 f.close()
302
303 #add to history
304 loginfo = {"op":set, "file":default_file, "line":total.count("\n")}
305 self.data.setVar(var, val, **loginfo)
306
307 def removeConfigurationVar(self, var):
308 conf_files = self.data.varhistory.get_variable_files(var)
309 topdir = self.data.getVar("TOPDIR")
310
311 for conf_file in conf_files:
312 if topdir in conf_file:
313 with open(conf_file, 'r') as f:
314 contents = f.readlines()
315 f.close()
316
317 lines = self.data.varhistory.get_variable_lines(var, conf_file)
318 for line in lines:
319 total = ""
320 i = 0
321 for c in contents:
322 total += c
323 i = i + 1
324 if i==int(line):
325 end_index = len(total)
326 index = total.rfind(var, 0, end_index)
327
328 begin_line = total.count("\n",0,index)
329
330 #check if the variable was saved before in the same way
331 if contents[begin_line-1]== "#added by hob\n":
332 contents[begin_line-1] = contents[begin_line] = "\n"
333 else:
334 contents[begin_line] = "\n"
335 #remove var from history
336 self.data.varhistory.del_var_history(var, conf_file, line)
337
338 total = ""
339 for c in contents:
340 total += c
341 with open(conf_file, 'w') as f:
342 f.write(total)
343 f.close()
344
345 def createConfigFile(self, name):
346 path = os.getcwd()
347 confpath = os.path.join(path, "conf", name)
348 open(confpath, 'w').close()
349
350 def parseConfiguration(self):
351
352 # Set log file verbosity
353 verboselogs = bb.utils.to_boolean(self.data.getVar("BB_VERBOSE_LOGS", "0"))
354 if verboselogs:
355 bb.msg.loggerVerboseLogs = True
356
357 # Change nice level if we're asked to
358 nice = self.data.getVar("BB_NICE_LEVEL", True)
359 if nice:
360 curnice = os.nice(0)
361 nice = int(nice) - curnice
362 buildlog.verbose("Renice to %s " % os.nice(nice))
363
364 if self.recipecache:
365 del self.recipecache
366 self.recipecache = bb.cache.CacheData(self.caches_array)
367
368 self.handleCollections( self.data.getVar("BBFILE_COLLECTIONS", True) )
369
370 def runCommands(self, server, data, abort):
371 """
372 Run any queued asynchronous command
373 This is done by the idle handler so it runs in true context rather than
374 tied to any UI.
375 """
376
377 return self.command.runAsyncCommand()
378
379 def showVersions(self):
380
381 pkg_pn = self.recipecache.pkg_pn
382 (latest_versions, preferred_versions) = bb.providers.findProviders(self.data, self.recipecache, pkg_pn)
383
384 logger.plain("%-35s %25s %25s", "Recipe Name", "Latest Version", "Preferred Version")
385 logger.plain("%-35s %25s %25s\n", "===========", "==============", "=================")
386
387 for p in sorted(pkg_pn):
388 pref = preferred_versions[p]
389 latest = latest_versions[p]
390
391 prefstr = pref[0][0] + ":" + pref[0][1] + '-' + pref[0][2]
392 lateststr = latest[0][0] + ":" + latest[0][1] + "-" + latest[0][2]
393
394 if pref == latest:
395 prefstr = ""
396
397 logger.plain("%-35s %25s %25s", p, lateststr, prefstr)
398
399 def showEnvironment(self, buildfile = None, pkgs_to_build = []):
400 """
401 Show the outer or per-package environment
402 """
403 fn = None
404 envdata = None
405
406 if buildfile:
407 # Parse the configuration here. We need to do it explicitly here since
408 # this showEnvironment() code path doesn't use the cache
409 self.parseConfiguration()
410
411 fn, cls = bb.cache.Cache.virtualfn2realfn(buildfile)
412 fn = self.matchFile(fn)
413 fn = bb.cache.Cache.realfn2virtual(fn, cls)
414 elif len(pkgs_to_build) == 1:
415 ignore = self.data.getVar("ASSUME_PROVIDED", True) or ""
416 if pkgs_to_build[0] in set(ignore.split()):
417 bb.fatal("%s is in ASSUME_PROVIDED" % pkgs_to_build[0])
418
419 taskdata, runlist, pkgs_to_build = self.buildTaskData(pkgs_to_build, None, self.configuration.abort)
420
421 targetid = taskdata.getbuild_id(pkgs_to_build[0])
422 fnid = taskdata.build_targets[targetid][0]
423 fn = taskdata.fn_index[fnid]
424 else:
425 envdata = self.data
426
427 if fn:
428 try:
429 envdata = bb.cache.Cache.loadDataFull(fn, self.collection.get_file_appends(fn), self.data)
430 except Exception as e:
431 parselog.exception("Unable to read %s", fn)
432 raise
433
434 # Display history
435 with closing(StringIO()) as env:
436 self.data.inchistory.emit(env)
437 logger.plain(env.getvalue())
438
439 # emit variables and shell functions
440 data.update_data(envdata)
441 with closing(StringIO()) as env:
442 data.emit_env(env, envdata, True)
443 logger.plain(env.getvalue())
444
445 # emit the metadata which isnt valid shell
446 data.expandKeys(envdata)
447 for e in envdata.keys():
448 if data.getVarFlag( e, 'python', envdata ):
449 logger.plain("\npython %s () {\n%s}\n", e, data.getVar(e, envdata, 1))
450
451
452 def buildTaskData(self, pkgs_to_build, task, abort):
453 """
454 Prepare a runqueue and taskdata object for iteration over pkgs_to_build
455 """
456 bb.event.fire(bb.event.TreeDataPreparationStarted(), self.data)
457
458 # A task of None means use the default task
459 if task is None:
460 task = self.configuration.cmd
461
462 fulltargetlist = self.checkPackages(pkgs_to_build)
463
464 localdata = data.createCopy(self.data)
465 bb.data.update_data(localdata)
466 bb.data.expandKeys(localdata)
467 taskdata = bb.taskdata.TaskData(abort, skiplist=self.skiplist)
468
469 current = 0
470 runlist = []
471 for k in fulltargetlist:
472 taskdata.add_provider(localdata, self.recipecache, k)
473 current += 1
474 runlist.append([k, "do_%s" % task])
475 bb.event.fire(bb.event.TreeDataPreparationProgress(current, len(fulltargetlist)), self.data)
476 taskdata.add_unresolved(localdata, self.recipecache)
477 bb.event.fire(bb.event.TreeDataPreparationCompleted(len(fulltargetlist)), self.data)
478 return taskdata, runlist, fulltargetlist
479
480 def prepareTreeData(self, pkgs_to_build, task):
481 """
482 Prepare a runqueue and taskdata object for iteration over pkgs_to_build
483 """
484
485 # We set abort to False here to prevent unbuildable targets raising
486 # an exception when we're just generating data
487 taskdata, runlist, pkgs_to_build = self.buildTaskData(pkgs_to_build, task, False)
488
489 return runlist, taskdata
490
491 ######## WARNING : this function requires cache_extra to be enabled ########
492
493 def generateTaskDepTreeData(self, pkgs_to_build, task):
494 """
495 Create a dependency graph of pkgs_to_build including reverse dependency
496 information.
497 """
498 runlist, taskdata = self.prepareTreeData(pkgs_to_build, task)
499 rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist)
500 rq.rqdata.prepare()
501 return self.buildDependTree(rq, taskdata)
502
503
504 def buildDependTree(self, rq, taskdata):
505 seen_fnids = []
506 depend_tree = {}
507 depend_tree["depends"] = {}
508 depend_tree["tdepends"] = {}
509 depend_tree["pn"] = {}
510 depend_tree["rdepends-pn"] = {}
511 depend_tree["packages"] = {}
512 depend_tree["rdepends-pkg"] = {}
513 depend_tree["rrecs-pkg"] = {}
514
515 for task in xrange(len(rq.rqdata.runq_fnid)):
516 taskname = rq.rqdata.runq_task[task]
517 fnid = rq.rqdata.runq_fnid[task]
518 fn = taskdata.fn_index[fnid]
519 pn = self.recipecache.pkg_fn[fn]
520 version = "%s:%s-%s" % self.recipecache.pkg_pepvpr[fn]
521 if pn not in depend_tree["pn"]:
522 depend_tree["pn"][pn] = {}
523 depend_tree["pn"][pn]["filename"] = fn
524 depend_tree["pn"][pn]["version"] = version
525
526 # if we have extra caches, list all attributes they bring in
527 extra_info = []
528 for cache_class in self.caches_array:
529 if type(cache_class) is type and issubclass(cache_class, bb.cache.RecipeInfoCommon) and hasattr(cache_class, 'cachefields'):
530 cachefields = getattr(cache_class, 'cachefields', [])
531 extra_info = extra_info + cachefields
532
533 # for all attributes stored, add them to the dependency tree
534 for ei in extra_info:
535 depend_tree["pn"][pn][ei] = vars(self.recipecache)[ei][fn]
536
537
538 for dep in rq.rqdata.runq_depends[task]:
539 depfn = taskdata.fn_index[rq.rqdata.runq_fnid[dep]]
540 deppn = self.recipecache.pkg_fn[depfn]
541 dotname = "%s.%s" % (pn, rq.rqdata.runq_task[task])
542 if not dotname in depend_tree["tdepends"]:
543 depend_tree["tdepends"][dotname] = []
544 depend_tree["tdepends"][dotname].append("%s.%s" % (deppn, rq.rqdata.runq_task[dep]))
545 if fnid not in seen_fnids:
546 seen_fnids.append(fnid)
547 packages = []
548
549 depend_tree["depends"][pn] = []
550 for dep in taskdata.depids[fnid]:
551 depend_tree["depends"][pn].append(taskdata.build_names_index[dep])
552
553 depend_tree["rdepends-pn"][pn] = []
554 for rdep in taskdata.rdepids[fnid]:
555 depend_tree["rdepends-pn"][pn].append(taskdata.run_names_index[rdep])
556
557 rdepends = self.recipecache.rundeps[fn]
558 for package in rdepends:
559 depend_tree["rdepends-pkg"][package] = []
560 for rdepend in rdepends[package]:
561 depend_tree["rdepends-pkg"][package].append(rdepend)
562 packages.append(package)
563
564 rrecs = self.recipecache.runrecs[fn]
565 for package in rrecs:
566 depend_tree["rrecs-pkg"][package] = []
567 for rdepend in rrecs[package]:
568 depend_tree["rrecs-pkg"][package].append(rdepend)
569 if not package in packages:
570 packages.append(package)
571
572 for package in packages:
573 if package not in depend_tree["packages"]:
574 depend_tree["packages"][package] = {}
575 depend_tree["packages"][package]["pn"] = pn
576 depend_tree["packages"][package]["filename"] = fn
577 depend_tree["packages"][package]["version"] = version
578
579 return depend_tree
580
581 ######## WARNING : this function requires cache_extra to be enabled ########
582 def generatePkgDepTreeData(self, pkgs_to_build, task):
583 """
584 Create a dependency tree of pkgs_to_build, returning the data.
585 """
586 _, taskdata = self.prepareTreeData(pkgs_to_build, task)
587 tasks_fnid = []
588 if len(taskdata.tasks_name) != 0:
589 for task in xrange(len(taskdata.tasks_name)):
590 tasks_fnid.append(taskdata.tasks_fnid[task])
591
592 seen_fnids = []
593 depend_tree = {}
594 depend_tree["depends"] = {}
595 depend_tree["pn"] = {}
596 depend_tree["rdepends-pn"] = {}
597 depend_tree["rdepends-pkg"] = {}
598 depend_tree["rrecs-pkg"] = {}
599
600 # if we have extra caches, list all attributes they bring in
601 extra_info = []
602 for cache_class in self.caches_array:
603 if type(cache_class) is type and issubclass(cache_class, bb.cache.RecipeInfoCommon) and hasattr(cache_class, 'cachefields'):
604 cachefields = getattr(cache_class, 'cachefields', [])
605 extra_info = extra_info + cachefields
606
607 for task in xrange(len(tasks_fnid)):
608 fnid = tasks_fnid[task]
609 fn = taskdata.fn_index[fnid]
610 pn = self.recipecache.pkg_fn[fn]
611
612 if pn not in depend_tree["pn"]:
613 depend_tree["pn"][pn] = {}
614 depend_tree["pn"][pn]["filename"] = fn
615 version = "%s:%s-%s" % self.recipecache.pkg_pepvpr[fn]
616 depend_tree["pn"][pn]["version"] = version
617 rdepends = self.recipecache.rundeps[fn]
618 rrecs = self.recipecache.runrecs[fn]
619 depend_tree["pn"][pn]["inherits"] = self.recipecache.inherits.get(fn, None)
620
621 # for all extra attributes stored, add them to the dependency tree
622 for ei in extra_info:
623 depend_tree["pn"][pn][ei] = vars(self.recipecache)[ei][fn]
624
625 if fnid not in seen_fnids:
626 seen_fnids.append(fnid)
627
628 depend_tree["depends"][pn] = []
629 for dep in taskdata.depids[fnid]:
630 item = taskdata.build_names_index[dep]
631 pn_provider = ""
632 targetid = taskdata.getbuild_id(item)
633 if targetid in taskdata.build_targets and taskdata.build_targets[targetid]:
634 id = taskdata.build_targets[targetid][0]
635 fn_provider = taskdata.fn_index[id]
636 pn_provider = self.recipecache.pkg_fn[fn_provider]
637 else:
638 pn_provider = item
639 depend_tree["depends"][pn].append(pn_provider)
640
641 depend_tree["rdepends-pn"][pn] = []
642 for rdep in taskdata.rdepids[fnid]:
643 item = taskdata.run_names_index[rdep]
644 pn_rprovider = ""
645 targetid = taskdata.getrun_id(item)
646 if targetid in taskdata.run_targets and taskdata.run_targets[targetid]:
647 id = taskdata.run_targets[targetid][0]
648 fn_rprovider = taskdata.fn_index[id]
649 pn_rprovider = self.recipecache.pkg_fn[fn_rprovider]
650 else:
651 pn_rprovider = item
652 depend_tree["rdepends-pn"][pn].append(pn_rprovider)
653
654 depend_tree["rdepends-pkg"].update(rdepends)
655 depend_tree["rrecs-pkg"].update(rrecs)
656
657 return depend_tree
658
659 def generateDepTreeEvent(self, pkgs_to_build, task):
660 """
661 Create a task dependency graph of pkgs_to_build.
662 Generate an event with the result
663 """
664 depgraph = self.generateTaskDepTreeData(pkgs_to_build, task)
665 bb.event.fire(bb.event.DepTreeGenerated(depgraph), self.data)
666
667 def generateDotGraphFiles(self, pkgs_to_build, task):
668 """
669 Create a task dependency graph of pkgs_to_build.
670 Save the result to a set of .dot files.
671 """
672
673 depgraph = self.generateTaskDepTreeData(pkgs_to_build, task)
674
675 # Prints a flattened form of package-depends below where subpackages of a package are merged into the main pn
676 depends_file = file('pn-depends.dot', 'w' )
677 buildlist_file = file('pn-buildlist', 'w' )
678 print("digraph depends {", file=depends_file)
679 for pn in depgraph["pn"]:
680 fn = depgraph["pn"][pn]["filename"]
681 version = depgraph["pn"][pn]["version"]
682 print('"%s" [label="%s %s\\n%s"]' % (pn, pn, version, fn), file=depends_file)
683 print("%s" % pn, file=buildlist_file)
684 buildlist_file.close()
685 logger.info("PN build list saved to 'pn-buildlist'")
686 for pn in depgraph["depends"]:
687 for depend in depgraph["depends"][pn]:
688 print('"%s" -> "%s"' % (pn, depend), file=depends_file)
689 for pn in depgraph["rdepends-pn"]:
690 for rdepend in depgraph["rdepends-pn"][pn]:
691 print('"%s" -> "%s" [style=dashed]' % (pn, rdepend), file=depends_file)
692 print("}", file=depends_file)
693 logger.info("PN dependencies saved to 'pn-depends.dot'")
694
695 depends_file = file('package-depends.dot', 'w' )
696 print("digraph depends {", file=depends_file)
697 for package in depgraph["packages"]:
698 pn = depgraph["packages"][package]["pn"]
699 fn = depgraph["packages"][package]["filename"]
700 version = depgraph["packages"][package]["version"]
701 if package == pn:
702 print('"%s" [label="%s %s\\n%s"]' % (pn, pn, version, fn), file=depends_file)
703 else:
704 print('"%s" [label="%s(%s) %s\\n%s"]' % (package, package, pn, version, fn), file=depends_file)
705 for depend in depgraph["depends"][pn]:
706 print('"%s" -> "%s"' % (package, depend), file=depends_file)
707 for package in depgraph["rdepends-pkg"]:
708 for rdepend in depgraph["rdepends-pkg"][package]:
709 print('"%s" -> "%s" [style=dashed]' % (package, rdepend), file=depends_file)
710 for package in depgraph["rrecs-pkg"]:
711 for rdepend in depgraph["rrecs-pkg"][package]:
712 print('"%s" -> "%s" [style=dashed]' % (package, rdepend), file=depends_file)
713 print("}", file=depends_file)
714 logger.info("Package dependencies saved to 'package-depends.dot'")
715
716 tdepends_file = file('task-depends.dot', 'w' )
717 print("digraph depends {", file=tdepends_file)
718 for task in depgraph["tdepends"]:
719 (pn, taskname) = task.rsplit(".", 1)
720 fn = depgraph["pn"][pn]["filename"]
721 version = depgraph["pn"][pn]["version"]
722 print('"%s.%s" [label="%s %s\\n%s\\n%s"]' % (pn, taskname, pn, taskname, version, fn), file=tdepends_file)
723 for dep in depgraph["tdepends"][task]:
724 print('"%s" -> "%s"' % (task, dep), file=tdepends_file)
725 print("}", file=tdepends_file)
726 logger.info("Task dependencies saved to 'task-depends.dot'")
727
728 def show_appends_with_no_recipes( self ):
729 recipes = set(os.path.basename(f)
730 for f in self.recipecache.pkg_fn.iterkeys())
731 recipes |= set(os.path.basename(f)
732 for f in self.skiplist.iterkeys())
733 appended_recipes = self.collection.appendlist.iterkeys()
734 appends_without_recipes = [self.collection.appendlist[recipe]
735 for recipe in appended_recipes
736 if recipe not in recipes]
737 if appends_without_recipes:
738 appendlines = (' %s' % append
739 for appends in appends_without_recipes
740 for append in appends)
741 msg = 'No recipes available for:\n%s' % '\n'.join(appendlines)
742 warn_only = data.getVar("BB_DANGLINGAPPENDS_WARNONLY", \
743 self.data, False) or "no"
744 if warn_only.lower() in ("1", "yes", "true"):
745 bb.warn(msg)
746 else:
747 bb.fatal(msg)
748
749 def handlePrefProviders(self):
750
751 localdata = data.createCopy(self.data)
752 bb.data.update_data(localdata)
753 bb.data.expandKeys(localdata)
754
755 # Handle PREFERRED_PROVIDERS
756 for p in (localdata.getVar('PREFERRED_PROVIDERS', True) or "").split():
757 try:
758 (providee, provider) = p.split(':')
759 except:
760 providerlog.critical("Malformed option in PREFERRED_PROVIDERS variable: %s" % p)
761 continue
762 if providee in self.recipecache.preferred and self.recipecache.preferred[providee] != provider:
763 providerlog.error("conflicting preferences for %s: both %s and %s specified", providee, provider, self.recipecache.preferred[providee])
764 self.recipecache.preferred[providee] = provider
765
766 def findCoreBaseFiles(self, subdir, configfile):
767 corebase = self.data.getVar('COREBASE', True) or ""
768 paths = []
769 for root, dirs, files in os.walk(corebase + '/' + subdir):
770 for d in dirs:
771 configfilepath = os.path.join(root, d, configfile)
772 if os.path.exists(configfilepath):
773 paths.append(os.path.join(root, d))
774
775 if paths:
776 bb.event.fire(bb.event.CoreBaseFilesFound(paths), self.data)
777
778 def findConfigFilePath(self, configfile):
779 """
780 Find the location on disk of configfile and if it exists and was parsed by BitBake
781 emit the ConfigFilePathFound event with the path to the file.
782 """
783 path = bb.cookerdata.findConfigFile(configfile, self.data)
784 if not path:
785 return
786
787 # Generate a list of parsed configuration files by searching the files
788 # listed in the __depends and __base_depends variables with a .conf suffix.
789 conffiles = []
790 dep_files = self.data.getVar('__base_depends') or []
791 dep_files = dep_files + (self.data.getVar('__depends') or [])
792
793 for f in dep_files:
794 if f[0].endswith(".conf"):
795 conffiles.append(f[0])
796
797 _, conf, conffile = path.rpartition("conf/")
798 match = os.path.join(conf, conffile)
799 # Try and find matches for conf/conffilename.conf as we don't always
800 # have the full path to the file.
801 for cfg in conffiles:
802 if cfg.endswith(match):
803 bb.event.fire(bb.event.ConfigFilePathFound(path),
804 self.data)
805 break
806
807 def findFilesMatchingInDir(self, filepattern, directory):
808 """
809 Searches for files matching the regex 'pattern' which are children of
810 'directory' in each BBPATH. i.e. to find all rootfs package classes available
811 to BitBake one could call findFilesMatchingInDir(self, 'rootfs_', 'classes')
812 or to find all machine configuration files one could call:
813 findFilesMatchingInDir(self, 'conf/machines', 'conf')
814 """
815 import re
816
817 matches = []
818 p = re.compile(re.escape(filepattern))
819 bbpaths = self.data.getVar('BBPATH', True).split(':')
820 for path in bbpaths:
821 dirpath = os.path.join(path, directory)
822 if os.path.exists(dirpath):
823 for root, dirs, files in os.walk(dirpath):
824 for f in files:
825 if p.search(f):
826 matches.append(f)
827
828 if matches:
829 bb.event.fire(bb.event.FilesMatchingFound(filepattern, matches), self.data)
830
831 def findConfigFiles(self, varname):
832 """
833 Find config files which are appropriate values for varname.
834 i.e. MACHINE, DISTRO
835 """
836 possible = []
837 var = varname.lower()
838
839 data = self.data
840 # iterate configs
841 bbpaths = data.getVar('BBPATH', True).split(':')
842 for path in bbpaths:
843 confpath = os.path.join(path, "conf", var)
844 if os.path.exists(confpath):
845 for root, dirs, files in os.walk(confpath):
846 # get all child files, these are appropriate values
847 for f in files:
848 val, sep, end = f.rpartition('.')
849 if end == 'conf':
850 possible.append(val)
851
852 if possible:
853 bb.event.fire(bb.event.ConfigFilesFound(var, possible), self.data)
854
855 def findInheritsClass(self, klass):
856 """
857 Find all recipes which inherit the specified class
858 """
859 pkg_list = []
860
861 for pfn in self.recipecache.pkg_fn:
862 inherits = self.recipecache.inherits.get(pfn, None)
863 if inherits and inherits.count(klass) > 0:
864 pkg_list.append(self.recipecache.pkg_fn[pfn])
865
866 return pkg_list
867
868 def generateTargetsTree(self, klass=None, pkgs=[]):
869 """
870 Generate a dependency tree of buildable targets
871 Generate an event with the result
872 """
873 # if the caller hasn't specified a pkgs list default to universe
874 if not len(pkgs):
875 pkgs = ['universe']
876 # if inherited_class passed ensure all recipes which inherit the
877 # specified class are included in pkgs
878 if klass:
879 extra_pkgs = self.findInheritsClass(klass)
880 pkgs = pkgs + extra_pkgs
881
882 # generate a dependency tree for all our packages
883 tree = self.generatePkgDepTreeData(pkgs, 'build')
884 bb.event.fire(bb.event.TargetsTreeGenerated(tree), self.data)
885
886 def buildWorldTargetList(self):
887 """
888 Build package list for "bitbake world"
889 """
890 parselog.debug(1, "collating packages for \"world\"")
891 for f in self.recipecache.possible_world:
892 terminal = True
893 pn = self.recipecache.pkg_fn[f]
894
895 for p in self.recipecache.pn_provides[pn]:
896 if p.startswith('virtual/'):
897 parselog.debug(2, "World build skipping %s due to %s provider starting with virtual/", f, p)
898 terminal = False
899 break
900 for pf in self.recipecache.providers[p]:
901 if self.recipecache.pkg_fn[pf] != pn:
902 parselog.debug(2, "World build skipping %s due to both us and %s providing %s", f, pf, p)
903 terminal = False
904 break
905 if terminal:
906 self.recipecache.world_target.add(pn)
907
908 def interactiveMode( self ):
909 """Drop off into a shell"""
910 try:
911 from bb import shell
912 except ImportError:
913 parselog.exception("Interactive mode not available")
914 sys.exit(1)
915 else:
916 shell.start( self )
917
918
919 def handleCollections( self, collections ):
920 """Handle collections"""
921 errors = False
922 self.recipecache.bbfile_config_priorities = []
923 if collections:
924 collection_priorities = {}
925 collection_depends = {}
926 collection_list = collections.split()
927 min_prio = 0
928 for c in collection_list:
929 # Get collection priority if defined explicitly
930 priority = self.data.getVar("BBFILE_PRIORITY_%s" % c, True)
931 if priority:
932 try:
933 prio = int(priority)
934 except ValueError:
935 parselog.error("invalid value for BBFILE_PRIORITY_%s: \"%s\"", c, priority)
936 errors = True
937 if min_prio == 0 or prio < min_prio:
938 min_prio = prio
939 collection_priorities[c] = prio
940 else:
941 collection_priorities[c] = None
942
943 # Check dependencies and store information for priority calculation
944 deps = self.data.getVar("LAYERDEPENDS_%s" % c, True)
945 if deps:
946 depnamelist = []
947 deplist = deps.split()
948 for dep in deplist:
949 depsplit = dep.split(':')
950 if len(depsplit) > 1:
951 try:
952 depver = int(depsplit[1])
953 except ValueError:
954 parselog.error("invalid version value in LAYERDEPENDS_%s: \"%s\"", c, dep)
955 errors = True
956 continue
957 else:
958 depver = None
959 dep = depsplit[0]
960 depnamelist.append(dep)
961
962 if dep in collection_list:
963 if depver:
964 layerver = self.data.getVar("LAYERVERSION_%s" % dep, True)
965 if layerver:
966 try:
967 lver = int(layerver)
968 except ValueError:
969 parselog.error("invalid value for LAYERVERSION_%s: \"%s\"", c, layerver)
970 errors = True
971 continue
972 if lver != depver:
973 parselog.error("Layer '%s' depends on version %d of layer '%s', but version %d is enabled in your configuration", c, depver, dep, lver)
974 errors = True
975 else:
976 parselog.error("Layer '%s' depends on version %d of layer '%s', which exists in your configuration but does not specify a version", c, depver, dep)
977 errors = True
978 else:
979 parselog.error("Layer '%s' depends on layer '%s', but this layer is not enabled in your configuration", c, dep)
980 errors = True
981 collection_depends[c] = depnamelist
982 else:
983 collection_depends[c] = []
984
985 # Recursively work out collection priorities based on dependencies
986 def calc_layer_priority(collection):
987 if not collection_priorities[collection]:
988 max_depprio = min_prio
989 for dep in collection_depends[collection]:
990 calc_layer_priority(dep)
991 depprio = collection_priorities[dep]
992 if depprio > max_depprio:
993 max_depprio = depprio
994 max_depprio += 1
995 parselog.debug(1, "Calculated priority of layer %s as %d", collection, max_depprio)
996 collection_priorities[collection] = max_depprio
997
998 # Calculate all layer priorities using calc_layer_priority and store in bbfile_config_priorities
999 for c in collection_list:
1000 calc_layer_priority(c)
1001 regex = self.data.getVar("BBFILE_PATTERN_%s" % c, True)
1002 if regex == None:
1003 parselog.error("BBFILE_PATTERN_%s not defined" % c)
1004 errors = True
1005 continue
1006 try:
1007 cre = re.compile(regex)
1008 except re.error:
1009 parselog.error("BBFILE_PATTERN_%s \"%s\" is not a valid regular expression", c, regex)
1010 errors = True
1011 continue
1012 self.recipecache.bbfile_config_priorities.append((c, regex, cre, collection_priorities[c]))
1013 if errors:
1014 # We've already printed the actual error(s)
1015 raise CollectionError("Errors during parsing layer configuration")
1016
1017 def buildSetVars(self):
1018 """
1019 Setup any variables needed before starting a build
1020 """
1021 if not self.data.getVar("BUILDNAME"):
1022 self.data.setVar("BUILDNAME", time.strftime('%Y%m%d%H%M'))
1023 self.data.setVar("BUILDSTART", time.strftime('%m/%d/%Y %H:%M:%S', time.gmtime()))
1024
1025 def matchFiles(self, bf):
1026 """
1027 Find the .bb files which match the expression in 'buildfile'.
1028 """
1029 if bf.startswith("/") or bf.startswith("../"):
1030 bf = os.path.abspath(bf)
1031
1032 self.collection = CookerCollectFiles(self.recipecache.bbfile_config_priorities)
1033 filelist, masked = self.collection.collect_bbfiles(self.data, self.event_data)
1034 try:
1035 os.stat(bf)
1036 bf = os.path.abspath(bf)
1037 return [bf]
1038 except OSError:
1039 regexp = re.compile(bf)
1040 matches = []
1041 for f in filelist:
1042 if regexp.search(f) and os.path.isfile(f):
1043 matches.append(f)
1044 return matches
1045
1046 def matchFile(self, buildfile):
1047 """
1048 Find the .bb file which matches the expression in 'buildfile'.
1049 Raise an error if multiple files
1050 """
1051 matches = self.matchFiles(buildfile)
1052 if len(matches) != 1:
1053 if matches:
1054 msg = "Unable to match '%s' to a specific recipe file - %s matches found:" % (buildfile, len(matches))
1055 if matches:
1056 for f in matches:
1057 msg += "\n %s" % f
1058 parselog.error(msg)
1059 else:
1060 parselog.error("Unable to find any recipe file matching '%s'" % buildfile)
1061 raise NoSpecificMatch
1062 return matches[0]
1063
1064 def buildFile(self, buildfile, task):
1065 """
1066 Build the file matching regexp buildfile
1067 """
1068
1069 # Too many people use -b because they think it's how you normally
1070 # specify a target to be built, so show a warning
1071 bb.warn("Buildfile specified, dependencies will not be handled. If this is not what you want, do not use -b / --buildfile.")
1072
1073 # Parse the configuration here. We need to do it explicitly here since
1074 # buildFile() doesn't use the cache
1075 self.parseConfiguration()
1076
1077 # If we are told to do the None task then query the default task
1078 if (task == None):
1079 task = self.configuration.cmd
1080
1081 fn, cls = bb.cache.Cache.virtualfn2realfn(buildfile)
1082 fn = self.matchFile(fn)
1083
1084 self.buildSetVars()
1085
1086 self.recipecache = bb.cache.CacheData(self.caches_array)
1087 infos = bb.cache.Cache.parse(fn, self.collection.get_file_appends(fn), \
1088 self.data,
1089 self.caches_array)
1090 infos = dict(infos)
1091
1092 fn = bb.cache.Cache.realfn2virtual(fn, cls)
1093 try:
1094 info_array = infos[fn]
1095 except KeyError:
1096 bb.fatal("%s does not exist" % fn)
1097
1098 if info_array[0].skipped:
1099 bb.fatal("%s was skipped: %s" % (fn, info_array[0].skipreason))
1100
1101 self.recipecache.add_from_recipeinfo(fn, info_array)
1102
1103 # Tweak some variables
1104 item = info_array[0].pn
1105 self.recipecache.ignored_dependencies = set()
1106 self.recipecache.bbfile_priority[fn] = 1
1107
1108 # Remove external dependencies
1109 self.recipecache.task_deps[fn]['depends'] = {}
1110 self.recipecache.deps[fn] = []
1111 self.recipecache.rundeps[fn] = []
1112 self.recipecache.runrecs[fn] = []
1113
1114 # Invalidate task for target if force mode active
1115 if self.configuration.force:
1116 logger.verbose("Invalidate task %s, %s", task, fn)
1117 bb.parse.siggen.invalidate_task('do_%s' % task, self.recipecache, fn)
1118
1119 # Setup taskdata structure
1120 taskdata = bb.taskdata.TaskData(self.configuration.abort)
1121 taskdata.add_provider(self.data, self.recipecache, item)
1122
1123 buildname = self.data.getVar("BUILDNAME")
1124 bb.event.fire(bb.event.BuildStarted(buildname, [item]), self.event_data)
1125
1126 # Execute the runqueue
1127 runlist = [[item, "do_%s" % task]]
1128
1129 rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist)
1130
1131 def buildFileIdle(server, rq, abort):
1132
1133 if abort or self.state == state.forceshutdown:
1134 rq.finish_runqueue(True)
1135 elif self.state == state.shutdown:
1136 rq.finish_runqueue(False)
1137 failures = 0
1138 try:
1139 retval = rq.execute_runqueue()
1140 except runqueue.TaskFailure as exc:
1141 failures += len(exc.args)
1142 retval = False
1143 except SystemExit as exc:
1144 self.command.finishAsyncCommand()
1145 return False
1146
1147 if not retval:
1148 bb.event.fire(bb.event.BuildCompleted(len(rq.rqdata.runq_fnid), buildname, item, failures), self.event_data)
1149 self.command.finishAsyncCommand()
1150 return False
1151 if retval is True:
1152 return True
1153 return retval
1154
1155 self.configuration.server_register_idlecallback(buildFileIdle, rq)
1156
1157 def buildTargets(self, targets, task):
1158 """
1159 Attempt to build the targets specified
1160 """
1161
1162 def buildTargetsIdle(server, rq, abort):
1163 if abort or self.state == state.forceshutdown:
1164 rq.finish_runqueue(True)
1165 elif self.state == state.shutdown:
1166 rq.finish_runqueue(False)
1167 failures = 0
1168 try:
1169 retval = rq.execute_runqueue()
1170 except runqueue.TaskFailure as exc:
1171 failures += len(exc.args)
1172 retval = False
1173 except SystemExit as exc:
1174 self.command.finishAsyncCommand()
1175 return False
1176
1177 if not retval:
1178 bb.event.fire(bb.event.BuildCompleted(len(rq.rqdata.runq_fnid), buildname, targets, failures), self.data)
1179 self.command.finishAsyncCommand()
1180 return False
1181 if retval is True:
1182 return True
1183 return retval
1184
1185 self.buildSetVars()
1186
1187 taskdata, runlist, fulltargetlist = self.buildTaskData(targets, task, self.configuration.abort)
1188
1189 buildname = self.data.getVar("BUILDNAME")
1190 bb.event.fire(bb.event.BuildStarted(buildname, fulltargetlist), self.data)
1191
1192 rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist)
1193 if 'universe' in targets:
1194 rq.rqdata.warn_multi_bb = True
1195
1196 self.configuration.server_register_idlecallback(buildTargetsIdle, rq)
1197
1198
1199 def getAllKeysWithFlags(self, flaglist):
1200 dump = {}
1201 for k in self.data.keys():
1202 try:
1203 v = self.data.getVar(k, True)
1204 if not k.startswith("__") and not isinstance(v, bb.data_smart.DataSmart):
1205 dump[k] = { 'v' : v }
1206 for d in flaglist:
1207 dump[k][d] = self.data.getVarFlag(k, d)
1208 except Exception as e:
1209 print(e)
1210 return dump
1211
1212
1213 def generateNewImage(self, image, base_image, package_queue, timestamp, description):
1214 '''
1215 Create a new image with a "require"/"inherit" base_image statement
1216 '''
1217 if timestamp:
1218 image_name = os.path.splitext(image)[0]
1219 timestr = time.strftime("-%Y%m%d-%H%M%S")
1220 dest = image_name + str(timestr) + ".bb"
1221 else:
1222 if not image.endswith(".bb"):
1223 dest = image + ".bb"
1224 else:
1225 dest = image
1226
1227 if base_image:
1228 with open(base_image, 'r') as f:
1229 require_line = f.readline()
1230
1231 with open(dest, "w") as imagefile:
1232 if base_image is None:
1233 imagefile.write("inherit image\n")
1234 else:
1235 topdir = self.data.getVar("TOPDIR")
1236 if topdir in base_image:
1237 base_image = require_line.split()[1]
1238 imagefile.write("require " + base_image + "\n")
1239 image_install = "IMAGE_INSTALL = \""
1240 for package in package_queue:
1241 image_install += str(package) + " "
1242 image_install += "\"\n"
1243 imagefile.write(image_install)
1244
1245 description_var = "DESCRIPTION = \"" + description + "\"\n"
1246 imagefile.write(description_var)
1247
1248 self.state = state.initial
1249 if timestamp:
1250 return timestr
1251
1252 # This is called for all async commands when self.state != running
1253 def updateCache(self):
1254 if self.state == state.running:
1255 return
1256
1257 if self.state in (state.shutdown, state.forceshutdown):
1258 self.parser.shutdown(clean=False, force = True)
1259 raise bb.BBHandledException()
1260
1261 if self.state != state.parsing:
1262 self.parseConfiguration ()
1263
1264 ignore = self.data.getVar("ASSUME_PROVIDED", True) or ""
1265 self.recipecache.ignored_dependencies = set(ignore.split())
1266
1267 for dep in self.configuration.extra_assume_provided:
1268 self.recipecache.ignored_dependencies.add(dep)
1269
1270 self.collection = CookerCollectFiles(self.recipecache.bbfile_config_priorities)
1271 (filelist, masked) = self.collection.collect_bbfiles(self.data, self.event_data)
1272
1273 self.data.renameVar("__depends", "__base_depends")
1274
1275 self.parser = CookerParser(self, filelist, masked)
1276 self.state = state.parsing
1277
1278 if not self.parser.parse_next():
1279 collectlog.debug(1, "parsing complete")
1280 if self.parser.error:
1281 raise bb.BBHandledException()
1282 self.show_appends_with_no_recipes()
1283 self.handlePrefProviders()
1284 self.recipecache.bbfile_priority = self.collection.collection_priorities(self.recipecache.pkg_fn)
1285 self.state = state.running
1286 return None
1287
1288 return True
1289
1290 def checkPackages(self, pkgs_to_build):
1291
1292 # Return a copy, don't modify the original
1293 pkgs_to_build = pkgs_to_build[:]
1294
1295 if len(pkgs_to_build) == 0:
1296 raise NothingToBuild
1297
1298 if 'world' in pkgs_to_build:
1299 self.buildWorldTargetList()
1300 pkgs_to_build.remove('world')
1301 for t in self.recipecache.world_target:
1302 pkgs_to_build.append(t)
1303
1304 if 'universe' in pkgs_to_build:
1305 parselog.warn("The \"universe\" target is only intended for testing and may produce errors.")
1306 parselog.debug(1, "collating packages for \"universe\"")
1307 pkgs_to_build.remove('universe')
1308 for t in self.recipecache.universe_target:
1309 pkgs_to_build.append(t)
1310
1311 return pkgs_to_build
1312
1313
1314
1315
1316 def pre_serve(self):
1317 # Empty the environment. The environment will be populated as
1318 # necessary from the data store.
1319 #bb.utils.empty_environment()
1320 try:
1321 self.prhost = prserv.serv.auto_start(self.data)
1322 except prserv.serv.PRServiceConfigError:
1323 bb.event.fire(CookerExit(), self.event_data)
1324 return
1325
1326 def post_serve(self):
1327 prserv.serv.auto_shutdown(self.data)
1328 bb.event.fire(CookerExit(), self.event_data)
1329
1330 def shutdown(self, force = False):
1331 if force:
1332 self.state = state.forceshutdown
1333 else:
1334 self.state = state.shutdown
1335
1336 def finishcommand(self):
1337 self.state = state.initial
1338
1339 def initialize(self):
1340 self.initConfigurationData()
1341
1342 def reset(self):
1343 self.loadConfigurationData()
1344
1345def server_main(cooker, func, *args):
1346 cooker.pre_serve()
1347
1348 if cooker.configuration.profile:
1349 try:
1350 import cProfile as profile
1351 except:
1352 import profile
1353 prof = profile.Profile()
1354
1355 ret = profile.Profile.runcall(prof, func, *args)
1356
1357 prof.dump_stats("profile.log")
1358 bb.utils.process_profilelog("profile.log")
1359 print("Raw profiling information saved to profile.log and processed statistics to profile.log.processed")
1360
1361 else:
1362 ret = func(*args)
1363
1364 cooker.post_serve()
1365
1366 return ret
1367
1368class CookerExit(bb.event.Event):
1369 """
1370 Notify clients of the Cooker shutdown
1371 """
1372
1373 def __init__(self):
1374 bb.event.Event.__init__(self)
1375
1376
1377class CookerCollectFiles(object):
1378 def __init__(self, priorities):
1379 self.appendlist = {}
1380 self.bbfile_config_priorities = priorities
1381
1382 def calc_bbfile_priority( self, filename, matched = None ):
1383 for _, _, regex, pri in self.bbfile_config_priorities:
1384 if regex.match(filename):
1385 if matched != None:
1386 if not regex in matched:
1387 matched.add(regex)
1388 return pri
1389 return 0
1390
1391 def get_bbfiles(self):
1392 """Get list of default .bb files by reading out the current directory"""
1393 path = os.getcwd()
1394 contents = os.listdir(path)
1395 bbfiles = []
1396 for f in contents:
1397 if f.endswith(".bb"):
1398 bbfiles.append(os.path.abspath(os.path.join(path, f)))
1399 return bbfiles
1400
1401 def find_bbfiles(self, path):
1402 """Find all the .bb and .bbappend files in a directory"""
1403 found = []
1404 for dir, dirs, files in os.walk(path):
1405 for ignored in ('SCCS', 'CVS', '.svn'):
1406 if ignored in dirs:
1407 dirs.remove(ignored)
1408 found += [os.path.join(dir, f) for f in files if (f.endswith('.bb') or f.endswith('.bbappend'))]
1409
1410 return found
1411
1412 def collect_bbfiles(self, config, eventdata):
1413 """Collect all available .bb build files"""
1414 masked = 0
1415
1416 collectlog.debug(1, "collecting .bb files")
1417
1418 files = (config.getVar( "BBFILES", True) or "").split()
1419 config.setVar("BBFILES", " ".join(files))
1420
1421 # Sort files by priority
1422 files.sort( key=lambda fileitem: self.calc_bbfile_priority(fileitem) )
1423
1424 if not len(files):
1425 files = self.get_bbfiles()
1426
1427 if not len(files):
1428 collectlog.error("no recipe files to build, check your BBPATH and BBFILES?")
1429 bb.event.fire(CookerExit(), eventdata)
1430
1431 # Can't use set here as order is important
1432 newfiles = []
1433 for f in files:
1434 if os.path.isdir(f):
1435 dirfiles = self.find_bbfiles(f)
1436 for g in dirfiles:
1437 if g not in newfiles:
1438 newfiles.append(g)
1439 else:
1440 globbed = glob.glob(f)
1441 if not globbed and os.path.exists(f):
1442 globbed = [f]
1443 for g in globbed:
1444 if g not in newfiles:
1445 newfiles.append(g)
1446
1447 bbmask = config.getVar('BBMASK', True)
1448
1449 if bbmask:
1450 try:
1451 bbmask_compiled = re.compile(bbmask)
1452 except sre_constants.error:
1453 collectlog.critical("BBMASK is not a valid regular expression, ignoring.")
1454 return list(newfiles), 0
1455
1456 bbfiles = []
1457 bbappend = []
1458 for f in newfiles:
1459 if bbmask and bbmask_compiled.search(f):
1460 collectlog.debug(1, "skipping masked file %s", f)
1461 masked += 1
1462 continue
1463 if f.endswith('.bb'):
1464 bbfiles.append(f)
1465 elif f.endswith('.bbappend'):
1466 bbappend.append(f)
1467 else:
1468 collectlog.debug(1, "skipping %s: unknown file extension", f)
1469
1470 # Build a list of .bbappend files for each .bb file
1471 for f in bbappend:
1472 base = os.path.basename(f).replace('.bbappend', '.bb')
1473 if not base in self.appendlist:
1474 self.appendlist[base] = []
1475 if f not in self.appendlist[base]:
1476 self.appendlist[base].append(f)
1477
1478 # Find overlayed recipes
1479 # bbfiles will be in priority order which makes this easy
1480 bbfile_seen = dict()
1481 self.overlayed = defaultdict(list)
1482 for f in reversed(bbfiles):
1483 base = os.path.basename(f)
1484 if base not in bbfile_seen:
1485 bbfile_seen[base] = f
1486 else:
1487 topfile = bbfile_seen[base]
1488 self.overlayed[topfile].append(f)
1489
1490 return (bbfiles, masked)
1491
1492 def get_file_appends(self, fn):
1493 """
1494 Returns a list of .bbappend files to apply to fn
1495 """
1496 f = os.path.basename(fn)
1497 if f in self.appendlist:
1498 return self.appendlist[f]
1499 return []
1500
1501 def collection_priorities(self, pkgfns):
1502
1503 priorities = {}
1504
1505 # Calculate priorities for each file
1506 matched = set()
1507 for p in pkgfns:
1508 realfn, cls = bb.cache.Cache.virtualfn2realfn(p)
1509 priorities[p] = self.calc_bbfile_priority(realfn, matched)
1510
1511 # Don't show the warning if the BBFILE_PATTERN did match .bbappend files
1512 unmatched = set()
1513 for _, _, regex, pri in self.bbfile_config_priorities:
1514 if not regex in matched:
1515 unmatched.add(regex)
1516
1517 def findmatch(regex):
1518 for bbfile in self.appendlist:
1519 for append in self.appendlist[bbfile]:
1520 if regex.match(append):
1521 return True
1522 return False
1523
1524 for unmatch in unmatched.copy():
1525 if findmatch(unmatch):
1526 unmatched.remove(unmatch)
1527
1528 for collection, pattern, regex, _ in self.bbfile_config_priorities:
1529 if regex in unmatched:
1530 collectlog.warn("No bb files matched BBFILE_PATTERN_%s '%s'" % (collection, pattern))
1531
1532 return priorities
1533
1534class ParsingFailure(Exception):
1535 def __init__(self, realexception, recipe):
1536 self.realexception = realexception
1537 self.recipe = recipe
1538 Exception.__init__(self, realexception, recipe)
1539
1540class Feeder(multiprocessing.Process):
1541 def __init__(self, jobs, to_parsers, quit):
1542 self.quit = quit
1543 self.jobs = jobs
1544 self.to_parsers = to_parsers
1545 multiprocessing.Process.__init__(self)
1546
1547 def run(self):
1548 while True:
1549 try:
1550 quit = self.quit.get_nowait()
1551 except Queue.Empty:
1552 pass
1553 else:
1554 if quit == 'cancel':
1555 self.to_parsers.cancel_join_thread()
1556 break
1557
1558 try:
1559 job = self.jobs.pop()
1560 except IndexError:
1561 break
1562
1563 try:
1564 self.to_parsers.put(job, timeout=0.5)
1565 except Queue.Full:
1566 self.jobs.insert(0, job)
1567 continue
1568
1569class Parser(multiprocessing.Process):
1570 def __init__(self, jobs, results, quit, init, profile):
1571 self.jobs = jobs
1572 self.results = results
1573 self.quit = quit
1574 self.init = init
1575 multiprocessing.Process.__init__(self)
1576 self.context = bb.utils.get_context().copy()
1577 self.handlers = bb.event.get_class_handlers().copy()
1578 self.profile = profile
1579
1580 def run(self):
1581
1582 if not self.profile:
1583 self.realrun()
1584 return
1585
1586 try:
1587 import cProfile as profile
1588 except:
1589 import profile
1590 prof = profile.Profile()
1591 try:
1592 profile.Profile.runcall(prof, self.realrun)
1593 finally:
1594 logfile = "profile-parse-%s.log" % multiprocessing.current_process().name
1595 prof.dump_stats(logfile)
1596 bb.utils.process_profilelog(logfile)
1597 print("Raw profiling information saved to %s and processed statistics to %s.processed" % (logfile, logfile))
1598
1599 def realrun(self):
1600 if self.init:
1601 self.init()
1602
1603 pending = []
1604 while True:
1605 try:
1606 self.quit.get_nowait()
1607 except Queue.Empty:
1608 pass
1609 else:
1610 self.results.cancel_join_thread()
1611 break
1612
1613 if pending:
1614 result = pending.pop()
1615 else:
1616 try:
1617 job = self.jobs.get(timeout=0.25)
1618 except Queue.Empty:
1619 continue
1620
1621 if job is None:
1622 break
1623 result = self.parse(*job)
1624
1625 try:
1626 self.results.put(result, timeout=0.25)
1627 except Queue.Full:
1628 pending.append(result)
1629
1630 def parse(self, filename, appends, caches_array):
1631 try:
1632 # Reset our environment and handlers to the original settings
1633 bb.utils.set_context(self.context.copy())
1634 bb.event.set_class_handlers(self.handlers.copy())
1635 return True, bb.cache.Cache.parse(filename, appends, self.cfg, caches_array)
1636 except Exception as exc:
1637 tb = sys.exc_info()[2]
1638 exc.recipe = filename
1639 exc.traceback = list(bb.exceptions.extract_traceback(tb, context=3))
1640 return True, exc
1641 # Need to turn BaseExceptions into Exceptions here so we gracefully shutdown
1642 # and for example a worker thread doesn't just exit on its own in response to
1643 # a SystemExit event for example.
1644 except BaseException as exc:
1645 return True, ParsingFailure(exc, filename)
1646
1647class CookerParser(object):
1648 def __init__(self, cooker, filelist, masked):
1649 self.filelist = filelist
1650 self.cooker = cooker
1651 self.cfgdata = cooker.data
1652 self.cfghash = cooker.data_hash
1653
1654 # Accounting statistics
1655 self.parsed = 0
1656 self.cached = 0
1657 self.error = 0
1658 self.masked = masked
1659
1660 self.skipped = 0
1661 self.virtuals = 0
1662 self.total = len(filelist)
1663
1664 self.current = 0
1665 self.num_processes = int(self.cfgdata.getVar("BB_NUMBER_PARSE_THREADS", True) or
1666 multiprocessing.cpu_count())
1667
1668 self.bb_cache = bb.cache.Cache(self.cfgdata, self.cfghash, cooker.caches_array)
1669 self.fromcache = []
1670 self.willparse = []
1671 for filename in self.filelist:
1672 appends = self.cooker.collection.get_file_appends(filename)
1673 if not self.bb_cache.cacheValid(filename, appends):
1674 self.willparse.append((filename, appends, cooker.caches_array))
1675 else:
1676 self.fromcache.append((filename, appends))
1677 self.toparse = self.total - len(self.fromcache)
1678 self.progress_chunk = max(self.toparse / 100, 1)
1679
1680 self.start()
1681 self.haveshutdown = False
1682
1683 def start(self):
1684 self.results = self.load_cached()
1685 self.processes = []
1686 if self.toparse:
1687 bb.event.fire(bb.event.ParseStarted(self.toparse), self.cfgdata)
1688 def init():
1689 Parser.cfg = self.cfgdata
1690 multiprocessing.util.Finalize(None, bb.codeparser.parser_cache_save, args=(self.cfgdata,), exitpriority=1)
1691 multiprocessing.util.Finalize(None, bb.fetch.fetcher_parse_save, args=(self.cfgdata,), exitpriority=1)
1692
1693 self.feeder_quit = multiprocessing.Queue(maxsize=1)
1694 self.parser_quit = multiprocessing.Queue(maxsize=self.num_processes)
1695 self.jobs = multiprocessing.Queue(maxsize=self.num_processes)
1696 self.result_queue = multiprocessing.Queue()
1697 self.feeder = Feeder(self.willparse, self.jobs, self.feeder_quit)
1698 self.feeder.start()
1699 for i in range(0, self.num_processes):
1700 parser = Parser(self.jobs, self.result_queue, self.parser_quit, init, self.cooker.configuration.profile)
1701 parser.start()
1702 self.processes.append(parser)
1703
1704 self.results = itertools.chain(self.results, self.parse_generator())
1705
1706 def shutdown(self, clean=True, force=False):
1707 if not self.toparse:
1708 return
1709 if self.haveshutdown:
1710 return
1711 self.haveshutdown = True
1712
1713 if clean:
1714 event = bb.event.ParseCompleted(self.cached, self.parsed,
1715 self.skipped, self.masked,
1716 self.virtuals, self.error,
1717 self.total)
1718
1719 bb.event.fire(event, self.cfgdata)
1720 self.feeder_quit.put(None)
1721 for process in self.processes:
1722 self.jobs.put(None)
1723 else:
1724 self.feeder_quit.put('cancel')
1725
1726 self.parser_quit.cancel_join_thread()
1727 for process in self.processes:
1728 self.parser_quit.put(None)
1729
1730 self.jobs.cancel_join_thread()
1731
1732 for process in self.processes:
1733 if force:
1734 process.join(.1)
1735 process.terminate()
1736 else:
1737 process.join()
1738 self.feeder.join()
1739
1740 sync = threading.Thread(target=self.bb_cache.sync)
1741 sync.start()
1742 multiprocessing.util.Finalize(None, sync.join, exitpriority=-100)
1743 bb.codeparser.parser_cache_savemerge(self.cooker.data)
1744 bb.fetch.fetcher_parse_done(self.cooker.data)
1745
1746 def load_cached(self):
1747 for filename, appends in self.fromcache:
1748 cached, infos = self.bb_cache.load(filename, appends, self.cfgdata)
1749 yield not cached, infos
1750
1751 def parse_generator(self):
1752 while True:
1753 if self.parsed >= self.toparse:
1754 break
1755
1756 try:
1757 result = self.result_queue.get(timeout=0.25)
1758 except Queue.Empty:
1759 pass
1760 else:
1761 value = result[1]
1762 if isinstance(value, BaseException):
1763 raise value
1764 else:
1765 yield result
1766
1767 def parse_next(self):
1768 result = []
1769 parsed = None
1770 try:
1771 parsed, result = self.results.next()
1772 except StopIteration:
1773 self.shutdown()
1774 return False
1775 except bb.BBHandledException as exc:
1776 self.error += 1
1777 logger.error('Failed to parse recipe: %s' % exc.recipe)
1778 self.shutdown(clean=False)
1779 return False
1780 except ParsingFailure as exc:
1781 self.error += 1
1782 logger.error('Unable to parse %s: %s' %
1783 (exc.recipe, bb.exceptions.to_string(exc.realexception)))
1784 self.shutdown(clean=False)
1785 return False
1786 except bb.parse.ParseError as exc:
1787 self.error += 1
1788 logger.error(str(exc))
1789 self.shutdown(clean=False)
1790 return False
1791 except bb.data_smart.ExpansionError as exc:
1792 self.error += 1
1793 _, value, _ = sys.exc_info()
1794 logger.error('ExpansionError during parsing %s: %s', value.recipe, str(exc))
1795 self.shutdown(clean=False)
1796 return False
1797 except SyntaxError as exc:
1798 self.error += 1
1799 logger.error('Unable to parse %s', exc.recipe)
1800 self.shutdown(clean=False)
1801 return False
1802 except Exception as exc:
1803 self.error += 1
1804 etype, value, tb = sys.exc_info()
1805 if hasattr(value, "recipe"):
1806 logger.error('Unable to parse %s', value.recipe,
1807 exc_info=(etype, value, exc.traceback))
1808 else:
1809 # Most likely, an exception occurred during raising an exception
1810 import traceback
1811 logger.error('Exception during parse: %s' % traceback.format_exc())
1812 self.shutdown(clean=False)
1813 return False
1814
1815 self.current += 1
1816 self.virtuals += len(result)
1817 if parsed:
1818 self.parsed += 1
1819 if self.parsed % self.progress_chunk == 0:
1820 bb.event.fire(bb.event.ParseProgress(self.parsed, self.toparse),
1821 self.cfgdata)
1822 else:
1823 self.cached += 1
1824
1825 for virtualfn, info_array in result:
1826 if info_array[0].skipped:
1827 self.skipped += 1
1828 self.cooker.skiplist[virtualfn] = SkippedPackage(info_array[0])
1829 self.bb_cache.add_info(virtualfn, info_array, self.cooker.recipecache,
1830 parsed=parsed)
1831 return True
1832
1833 def reparse(self, filename):
1834 infos = self.bb_cache.parse(filename,
1835 self.cooker.collection.get_file_appends(filename),
1836 self.cfgdata, self.cooker.caches_array)
1837 for vfn, info_array in infos:
1838 self.cooker.recipecache.add_from_recipeinfo(vfn, info_array)
diff --git a/bitbake/lib/bb/cookerdata.py b/bitbake/lib/bb/cookerdata.py
new file mode 100644
index 0000000000..e640ed0f35
--- /dev/null
+++ b/bitbake/lib/bb/cookerdata.py
@@ -0,0 +1,304 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# Copyright (C) 2003, 2004 Chris Larson
6# Copyright (C) 2003, 2004 Phil Blundell
7# Copyright (C) 2003 - 2005 Michael 'Mickey' Lauer
8# Copyright (C) 2005 Holger Hans Peter Freyther
9# Copyright (C) 2005 ROAD GmbH
10# Copyright (C) 2006 Richard Purdie
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License version 2 as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along
22# with this program; if not, write to the Free Software Foundation, Inc.,
23# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24
25import os, sys
26from functools import wraps
27import logging
28import bb
29from bb import data
30import bb.parse
31
32logger = logging.getLogger("BitBake")
33parselog = logging.getLogger("BitBake.Parsing")
34
35class ConfigParameters(object):
36 def __init__(self):
37 self.options, targets = self.parseCommandLine()
38 self.environment = self.parseEnvironment()
39
40 self.options.pkgs_to_build = targets or []
41
42 self.options.tracking = False
43 if hasattr(self.options, "show_environment") and self.options.show_environment:
44 self.options.tracking = True
45
46 for key, val in self.options.__dict__.items():
47 setattr(self, key, val)
48
49 def parseCommandLine(self):
50 raise Exception("Caller must implement commandline option parsing")
51
52 def parseEnvironment(self):
53 return os.environ.copy()
54
55 def updateFromServer(self, server):
56 if not self.options.cmd:
57 defaulttask, error = server.runCommand(["getVariable", "BB_DEFAULT_TASK"])
58 if error:
59 raise Exception("Unable to get the value of BB_DEFAULT_TASK from the server: %s" % error)
60 self.options.cmd = defaulttask or "build"
61 _, error = server.runCommand(["setConfig", "cmd", self.options.cmd])
62 if error:
63 raise Exception("Unable to set configuration option 'cmd' on the server: %s" % error)
64
65 if not self.options.pkgs_to_build:
66 bbpkgs, error = server.runCommand(["getVariable", "BBPKGS"])
67 if error:
68 raise Exception("Unable to get the value of BBPKGS from the server: %s" % error)
69 if bbpkgs:
70 self.options.pkgs_to_build.extend(bbpkgs.split())
71
72 def parseActions(self):
73 # Parse any commandline into actions
74 action = {'action':None, 'msg':None}
75 if self.options.show_environment:
76 if 'world' in self.options.pkgs_to_build:
77 action['msg'] = "'world' is not a valid target for --environment."
78 elif 'universe' in self.options.pkgs_to_build:
79 action['msg'] = "'universe' is not a valid target for --environment."
80 elif len(self.options.pkgs_to_build) > 1:
81 action['msg'] = "Only one target can be used with the --environment option."
82 elif self.options.buildfile and len(self.options.pkgs_to_build) > 0:
83 action['msg'] = "No target should be used with the --environment and --buildfile options."
84 elif len(self.options.pkgs_to_build) > 0:
85 action['action'] = ["showEnvironmentTarget", self.options.pkgs_to_build]
86 else:
87 action['action'] = ["showEnvironment", self.options.buildfile]
88 elif self.options.buildfile is not None:
89 action['action'] = ["buildFile", self.options.buildfile, self.options.cmd]
90 elif self.options.revisions_changed:
91 action['action'] = ["compareRevisions"]
92 elif self.options.show_versions:
93 action['action'] = ["showVersions"]
94 elif self.options.parse_only:
95 action['action'] = ["parseFiles"]
96 elif self.options.dot_graph:
97 if self.options.pkgs_to_build:
98 action['action'] = ["generateDotGraph", self.options.pkgs_to_build, self.options.cmd]
99 else:
100 action['msg'] = "Please specify a package name for dependency graph generation."
101 else:
102 if self.options.pkgs_to_build:
103 action['action'] = ["buildTargets", self.options.pkgs_to_build, self.options.cmd]
104 else:
105 #action['msg'] = "Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information."
106 action = None
107 self.options.initialaction = action
108 return action
109
110class CookerConfiguration(object):
111 """
112 Manages build options and configurations for one run
113 """
114
115 def __init__(self):
116 self.debug_domains = []
117 self.extra_assume_provided = []
118 self.prefile = []
119 self.postfile = []
120 self.debug = 0
121 self.cmd = None
122 self.abort = True
123 self.force = False
124 self.profile = False
125 self.nosetscene = False
126 self.invalidate_stamp = False
127 self.dump_signatures = False
128 self.dry_run = False
129 self.tracking = False
130
131 self.env = {}
132
133 def setConfigParameters(self, parameters):
134 for key in self.__dict__.keys():
135 if key in parameters.options.__dict__:
136 setattr(self, key, parameters.options.__dict__[key])
137 self.env = parameters.environment.copy()
138 self.tracking = parameters.tracking
139
140 def setServerRegIdleCallback(self, srcb):
141 self.server_register_idlecallback = srcb
142
143 def __getstate__(self):
144 state = {}
145 for key in self.__dict__.keys():
146 if key == "server_register_idlecallback":
147 state[key] = None
148 else:
149 state[key] = getattr(self, key)
150 return state
151
152 def __setstate__(self,state):
153 for k in state:
154 setattr(self, k, state[k])
155
156
157def catch_parse_error(func):
158 """Exception handling bits for our parsing"""
159 @wraps(func)
160 def wrapped(fn, *args):
161 try:
162 return func(fn, *args)
163 except (IOError, bb.parse.ParseError, bb.data_smart.ExpansionError) as exc:
164 import traceback
165 parselog.critical( traceback.format_exc())
166 parselog.critical("Unable to parse %s: %s" % (fn, exc))
167 sys.exit(1)
168 return wrapped
169
170@catch_parse_error
171def parse_config_file(fn, data, include=True):
172 return bb.parse.handle(fn, data, include)
173
174@catch_parse_error
175def _inherit(bbclass, data):
176 bb.parse.BBHandler.inherit(bbclass, "configuration INHERITs", 0, data)
177 return data
178
179def findConfigFile(configfile, data):
180 search = []
181 bbpath = data.getVar("BBPATH", True)
182 if bbpath:
183 for i in bbpath.split(":"):
184 search.append(os.path.join(i, "conf", configfile))
185 path = os.getcwd()
186 while path != "/":
187 search.append(os.path.join(path, "conf", configfile))
188 path, _ = os.path.split(path)
189
190 for i in search:
191 if os.path.exists(i):
192 return i
193
194 return None
195
196class CookerDataBuilder(object):
197
198 def __init__(self, cookercfg, worker = False):
199
200 self.prefiles = cookercfg.prefile
201 self.postfiles = cookercfg.postfile
202 self.tracking = cookercfg.tracking
203
204 bb.utils.set_context(bb.utils.clean_context())
205 bb.event.set_class_handlers(bb.event.clean_class_handlers())
206 self.data = bb.data.init()
207 if self.tracking:
208 self.data.enableTracking()
209
210 # Keep a datastore of the initial environment variables and their
211 # values from when BitBake was launched to enable child processes
212 # to use environment variables which have been cleaned from the
213 # BitBake processes env
214 self.savedenv = bb.data.init()
215 for k in cookercfg.env:
216 self.savedenv.setVar(k, cookercfg.env[k])
217
218 filtered_keys = bb.utils.approved_variables()
219 bb.data.inheritFromOS(self.data, self.savedenv, filtered_keys)
220 self.data.setVar("BB_ORIGENV", self.savedenv)
221
222 if worker:
223 self.data.setVar("BB_WORKERCONTEXT", "1")
224
225 def parseBaseConfiguration(self):
226 try:
227 self.parseConfigurationFiles(self.prefiles, self.postfiles)
228 except SyntaxError:
229 sys.exit(1)
230 except Exception:
231 logger.exception("Error parsing configuration files")
232 sys.exit(1)
233
234 def _findLayerConf(self, data):
235 return findConfigFile("bblayers.conf", data)
236
237 def parseConfigurationFiles(self, prefiles, postfiles):
238 data = self.data
239 bb.parse.init_parser(data)
240
241 # Parse files for loading *before* bitbake.conf and any includes
242 for f in prefiles:
243 data = parse_config_file(f, data)
244
245 layerconf = self._findLayerConf(data)
246 if layerconf:
247 parselog.debug(2, "Found bblayers.conf (%s)", layerconf)
248 # By definition bblayers.conf is in conf/ of TOPDIR.
249 # We may have been called with cwd somewhere else so reset TOPDIR
250 data.setVar("TOPDIR", os.path.dirname(os.path.dirname(layerconf)))
251 data = parse_config_file(layerconf, data)
252
253 layers = (data.getVar('BBLAYERS', True) or "").split()
254
255 data = bb.data.createCopy(data)
256 for layer in layers:
257 parselog.debug(2, "Adding layer %s", layer)
258 data.setVar('LAYERDIR', layer)
259 data = parse_config_file(os.path.join(layer, "conf", "layer.conf"), data)
260 data.expandVarref('LAYERDIR')
261
262 data.delVar('LAYERDIR')
263
264 if not data.getVar("BBPATH", True):
265 msg = "The BBPATH variable is not set"
266 if not layerconf:
267 msg += (" and bitbake did not find a conf/bblayers.conf file in"
268 " the expected location.\nMaybe you accidentally"
269 " invoked bitbake from the wrong directory?")
270 raise SystemExit(msg)
271
272 data = parse_config_file(os.path.join("conf", "bitbake.conf"), data)
273
274 # Parse files for loading *after* bitbake.conf and any includes
275 for p in postfiles:
276 data = parse_config_file(p, data)
277
278 # Handle any INHERITs and inherit the base class
279 bbclasses = ["base"] + (data.getVar('INHERIT', True) or "").split()
280 for bbclass in bbclasses:
281 data = _inherit(bbclass, data)
282
283 # Nomally we only register event handlers at the end of parsing .bb files
284 # We register any handlers we've found so far here...
285 for var in data.getVar('__BBHANDLERS') or []:
286 bb.event.register(var, data.getVar(var), (data.getVarFlag(var, "eventmask", True) or "").split())
287
288 if data.getVar("BB_WORKERCONTEXT", False) is None:
289 bb.fetch.fetcher_init(data)
290 bb.codeparser.parser_cache_init(data)
291 bb.event.fire(bb.event.ConfigParsed(), data)
292
293 if data.getVar("BB_INVALIDCONF") is True:
294 data.setVar("BB_INVALIDCONF", False)
295 self.parseConfigurationFiles(self.prefiles, self.postfiles)
296 return
297
298 bb.parse.init_parser(data)
299 data.setVar('BBINCLUDED',bb.parse.get_file_depends(data))
300 self.data = data
301 self.data_hash = data.get_hash()
302
303
304
diff --git a/bitbake/lib/bb/daemonize.py b/bitbake/lib/bb/daemonize.py
new file mode 100644
index 0000000000..f0714b3af6
--- /dev/null
+++ b/bitbake/lib/bb/daemonize.py
@@ -0,0 +1,190 @@
1"""
2Python Deamonizing helper
3
4Configurable daemon behaviors:
5
6 1.) The current working directory set to the "/" directory.
7 2.) The current file creation mode mask set to 0.
8 3.) Close all open files (1024).
9 4.) Redirect standard I/O streams to "/dev/null".
10
11A failed call to fork() now raises an exception.
12
13References:
14 1) Advanced Programming in the Unix Environment: W. Richard Stevens
15 2) Unix Programming Frequently Asked Questions:
16 http://www.erlenstar.demon.co.uk/unix/faq_toc.html
17
18Modified to allow a function to be daemonized and return for
19bitbake use by Richard Purdie
20"""
21
22__author__ = "Chad J. Schroeder"
23__copyright__ = "Copyright (C) 2005 Chad J. Schroeder"
24__version__ = "0.2"
25
26# Standard Python modules.
27import os # Miscellaneous OS interfaces.
28import sys # System-specific parameters and functions.
29
30# Default daemon parameters.
31# File mode creation mask of the daemon.
32# For BitBake's children, we do want to inherit the parent umask.
33UMASK = None
34
35# Default maximum for the number of available file descriptors.
36MAXFD = 1024
37
38# The standard I/O file descriptors are redirected to /dev/null by default.
39if (hasattr(os, "devnull")):
40 REDIRECT_TO = os.devnull
41else:
42 REDIRECT_TO = "/dev/null"
43
44def createDaemon(function, logfile):
45 """
46 Detach a process from the controlling terminal and run it in the
47 background as a daemon, returning control to the caller.
48 """
49
50 try:
51 # Fork a child process so the parent can exit. This returns control to
52 # the command-line or shell. It also guarantees that the child will not
53 # be a process group leader, since the child receives a new process ID
54 # and inherits the parent's process group ID. This step is required
55 # to insure that the next call to os.setsid is successful.
56 pid = os.fork()
57 except OSError as e:
58 raise Exception("%s [%d]" % (e.strerror, e.errno))
59
60 if (pid == 0): # The first child.
61 # To become the session leader of this new session and the process group
62 # leader of the new process group, we call os.setsid(). The process is
63 # also guaranteed not to have a controlling terminal.
64 os.setsid()
65
66 # Is ignoring SIGHUP necessary?
67 #
68 # It's often suggested that the SIGHUP signal should be ignored before
69 # the second fork to avoid premature termination of the process. The
70 # reason is that when the first child terminates, all processes, e.g.
71 # the second child, in the orphaned group will be sent a SIGHUP.
72 #
73 # "However, as part of the session management system, there are exactly
74 # two cases where SIGHUP is sent on the death of a process:
75 #
76 # 1) When the process that dies is the session leader of a session that
77 # is attached to a terminal device, SIGHUP is sent to all processes
78 # in the foreground process group of that terminal device.
79 # 2) When the death of a process causes a process group to become
80 # orphaned, and one or more processes in the orphaned group are
81 # stopped, then SIGHUP and SIGCONT are sent to all members of the
82 # orphaned group." [2]
83 #
84 # The first case can be ignored since the child is guaranteed not to have
85 # a controlling terminal. The second case isn't so easy to dismiss.
86 # The process group is orphaned when the first child terminates and
87 # POSIX.1 requires that every STOPPED process in an orphaned process
88 # group be sent a SIGHUP signal followed by a SIGCONT signal. Since the
89 # second child is not STOPPED though, we can safely forego ignoring the
90 # SIGHUP signal. In any case, there are no ill-effects if it is ignored.
91 #
92 # import signal # Set handlers for asynchronous events.
93 # signal.signal(signal.SIGHUP, signal.SIG_IGN)
94
95 try:
96 # Fork a second child and exit immediately to prevent zombies. This
97 # causes the second child process to be orphaned, making the init
98 # process responsible for its cleanup. And, since the first child is
99 # a session leader without a controlling terminal, it's possible for
100 # it to acquire one by opening a terminal in the future (System V-
101 # based systems). This second fork guarantees that the child is no
102 # longer a session leader, preventing the daemon from ever acquiring
103 # a controlling terminal.
104 pid = os.fork() # Fork a second child.
105 except OSError as e:
106 raise Exception("%s [%d]" % (e.strerror, e.errno))
107
108 if (pid == 0): # The second child.
109 # We probably don't want the file mode creation mask inherited from
110 # the parent, so we give the child complete control over permissions.
111 if UMASK is not None:
112 os.umask(UMASK)
113 else:
114 # Parent (the first child) of the second child.
115 os._exit(0)
116 else:
117 # exit() or _exit()?
118 # _exit is like exit(), but it doesn't call any functions registered
119 # with atexit (and on_exit) or any registered signal handlers. It also
120 # closes any open file descriptors. Using exit() may cause all stdio
121 # streams to be flushed twice and any temporary files may be unexpectedly
122 # removed. It's therefore recommended that child branches of a fork()
123 # and the parent branch(es) of a daemon use _exit().
124 return
125
126 # Close all open file descriptors. This prevents the child from keeping
127 # open any file descriptors inherited from the parent. There is a variety
128 # of methods to accomplish this task. Three are listed below.
129 #
130 # Try the system configuration variable, SC_OPEN_MAX, to obtain the maximum
131 # number of open file descriptors to close. If it doesn't exists, use
132 # the default value (configurable).
133 #
134 # try:
135 # maxfd = os.sysconf("SC_OPEN_MAX")
136 # except (AttributeError, ValueError):
137 # maxfd = MAXFD
138 #
139 # OR
140 #
141 # if (os.sysconf_names.has_key("SC_OPEN_MAX")):
142 # maxfd = os.sysconf("SC_OPEN_MAX")
143 # else:
144 # maxfd = MAXFD
145 #
146 # OR
147 #
148 # Use the getrlimit method to retrieve the maximum file descriptor number
149 # that can be opened by this process. If there is not limit on the
150 # resource, use the default value.
151 #
152 import resource # Resource usage information.
153 maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
154 if (maxfd == resource.RLIM_INFINITY):
155 maxfd = MAXFD
156
157 # Iterate through and close all file descriptors.
158# for fd in range(0, maxfd):
159# try:
160# os.close(fd)
161# except OSError: # ERROR, fd wasn't open to begin with (ignored)
162# pass
163
164 # Redirect the standard I/O file descriptors to the specified file. Since
165 # the daemon has no controlling terminal, most daemons redirect stdin,
166 # stdout, and stderr to /dev/null. This is done to prevent side-effects
167 # from reads and writes to the standard I/O file descriptors.
168
169 # This call to open is guaranteed to return the lowest file descriptor,
170 # which will be 0 (stdin), since it was closed above.
171# os.open(REDIRECT_TO, os.O_RDWR) # standard input (0)
172
173 # Duplicate standard input to standard output and standard error.
174# os.dup2(0, 1) # standard output (1)
175# os.dup2(0, 2) # standard error (2)
176
177
178 si = file('/dev/null', 'r')
179 so = file(logfile, 'w')
180 se = so
181
182
183 # Replace those fds with our own
184 os.dup2(si.fileno(), sys.stdin.fileno())
185 os.dup2(so.fileno(), sys.stdout.fileno())
186 os.dup2(se.fileno(), sys.stderr.fileno())
187
188 function()
189
190 os._exit(0)
diff --git a/bitbake/lib/bb/data.py b/bitbake/lib/bb/data.py
new file mode 100644
index 0000000000..349fcfe878
--- /dev/null
+++ b/bitbake/lib/bb/data.py
@@ -0,0 +1,375 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Data' implementations
5
6Functions for interacting with the data structure used by the
7BitBake build tools.
8
9The expandData and update_data are the most expensive
10operations. At night the cookie monster came by and
11suggested 'give me cookies on setting the variables and
12things will work out'. Taking this suggestion into account
13applying the skills from the not yet passed 'Entwurf und
14Analyse von Algorithmen' lecture and the cookie
15monster seems to be right. We will track setVar more carefully
16to have faster update_data and expandKeys operations.
17
18This is a treade-off between speed and memory again but
19the speed is more critical here.
20"""
21
22# Copyright (C) 2003, 2004 Chris Larson
23# Copyright (C) 2005 Holger Hans Peter Freyther
24#
25# This program is free software; you can redistribute it and/or modify
26# it under the terms of the GNU General Public License version 2 as
27# published by the Free Software Foundation.
28#
29# This program is distributed in the hope that it will be useful,
30# but WITHOUT ANY WARRANTY; without even the implied warranty of
31# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32# GNU General Public License for more details.
33#
34# You should have received a copy of the GNU General Public License along
35# with this program; if not, write to the Free Software Foundation, Inc.,
36# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
37#
38#Based on functions from the base bb module, Copyright 2003 Holger Schurig
39
40import sys, os, re
41if sys.argv[0][-5:] == "pydoc":
42 path = os.path.dirname(os.path.dirname(sys.argv[1]))
43else:
44 path = os.path.dirname(os.path.dirname(sys.argv[0]))
45sys.path.insert(0, path)
46from itertools import groupby
47
48from bb import data_smart
49from bb import codeparser
50import bb
51
52logger = data_smart.logger
53_dict_type = data_smart.DataSmart
54
55def init():
56 """Return a new object representing the Bitbake data"""
57 return _dict_type()
58
59def init_db(parent = None):
60 """Return a new object representing the Bitbake data,
61 optionally based on an existing object"""
62 if parent is not None:
63 return parent.createCopy()
64 else:
65 return _dict_type()
66
67def createCopy(source):
68 """Link the source set to the destination
69 If one does not find the value in the destination set,
70 search will go on to the source set to get the value.
71 Value from source are copy-on-write. i.e. any try to
72 modify one of them will end up putting the modified value
73 in the destination set.
74 """
75 return source.createCopy()
76
77def initVar(var, d):
78 """Non-destructive var init for data structure"""
79 d.initVar(var)
80
81
82def setVar(var, value, d):
83 """Set a variable to a given value"""
84 d.setVar(var, value)
85
86
87def getVar(var, d, exp = 0):
88 """Gets the value of a variable"""
89 return d.getVar(var, exp)
90
91
92def renameVar(key, newkey, d):
93 """Renames a variable from key to newkey"""
94 d.renameVar(key, newkey)
95
96def delVar(var, d):
97 """Removes a variable from the data set"""
98 d.delVar(var)
99
100def appendVar(var, value, d):
101 """Append additional value to a variable"""
102 d.appendVar(var, value)
103
104def setVarFlag(var, flag, flagvalue, d):
105 """Set a flag for a given variable to a given value"""
106 d.setVarFlag(var, flag, flagvalue)
107
108def getVarFlag(var, flag, d):
109 """Gets given flag from given var"""
110 return d.getVarFlag(var, flag)
111
112def delVarFlag(var, flag, d):
113 """Removes a given flag from the variable's flags"""
114 d.delVarFlag(var, flag)
115
116def setVarFlags(var, flags, d):
117 """Set the flags for a given variable
118
119 Note:
120 setVarFlags will not clear previous
121 flags. Think of this method as
122 addVarFlags
123 """
124 d.setVarFlags(var, flags)
125
126def getVarFlags(var, d):
127 """Gets a variable's flags"""
128 return d.getVarFlags(var)
129
130def delVarFlags(var, d):
131 """Removes a variable's flags"""
132 d.delVarFlags(var)
133
134def keys(d):
135 """Return a list of keys in d"""
136 return d.keys()
137
138
139__expand_var_regexp__ = re.compile(r"\${[^{}]+}")
140__expand_python_regexp__ = re.compile(r"\${@.+?}")
141
142def expand(s, d, varname = None):
143 """Variable expansion using the data store"""
144 return d.expand(s, varname)
145
146def expandKeys(alterdata, readdata = None):
147 if readdata == None:
148 readdata = alterdata
149
150 todolist = {}
151 for key in alterdata:
152 if not '${' in key:
153 continue
154
155 ekey = expand(key, readdata)
156 if key == ekey:
157 continue
158 todolist[key] = ekey
159
160 # These two for loops are split for performance to maximise the
161 # usefulness of the expand cache
162
163 for key in todolist:
164 ekey = todolist[key]
165 newval = alterdata.getVar(ekey, 0)
166 if newval:
167 val = alterdata.getVar(key, 0)
168 if val is not None and newval is not None:
169 bb.warn("Variable key %s (%s) replaces original key %s (%s)." % (key, val, ekey, newval))
170 alterdata.renameVar(key, ekey)
171
172def inheritFromOS(d, savedenv, permitted):
173 """Inherit variables from the initial environment."""
174 exportlist = bb.utils.preserved_envvars_exported()
175 for s in savedenv.keys():
176 if s in permitted:
177 try:
178 d.setVar(s, getVar(s, savedenv, True), op = 'from env')
179 if s in exportlist:
180 d.setVarFlag(s, "export", True, op = 'auto env export')
181 except TypeError:
182 pass
183
184def emit_var(var, o=sys.__stdout__, d = init(), all=False):
185 """Emit a variable to be sourced by a shell."""
186 if getVarFlag(var, "python", d):
187 return 0
188
189 export = getVarFlag(var, "export", d)
190 unexport = getVarFlag(var, "unexport", d)
191 func = getVarFlag(var, "func", d)
192 if not all and not export and not unexport and not func:
193 return 0
194
195 try:
196 if all:
197 oval = getVar(var, d, 0)
198 val = getVar(var, d, 1)
199 except (KeyboardInterrupt, bb.build.FuncFailed):
200 raise
201 except Exception as exc:
202 o.write('# expansion of %s threw %s: %s\n' % (var, exc.__class__.__name__, str(exc)))
203 return 0
204
205 if all:
206 d.varhistory.emit(var, oval, val, o)
207
208 if (var.find("-") != -1 or var.find(".") != -1 or var.find('{') != -1 or var.find('}') != -1 or var.find('+') != -1) and not all:
209 return 0
210
211 varExpanded = expand(var, d)
212
213 if unexport:
214 o.write('unset %s\n' % varExpanded)
215 return 0
216
217 if not val:
218 return 0
219
220 val = str(val)
221
222 if func:
223 # NOTE: should probably check for unbalanced {} within the var
224 o.write("%s() {\n%s\n}\n" % (varExpanded, val))
225 return 1
226
227 if export:
228 o.write('export ')
229
230 # if we're going to output this within doublequotes,
231 # to a shell, we need to escape the quotes in the var
232 alter = re.sub('"', '\\"', val.strip())
233 alter = re.sub('\n', ' \\\n', alter)
234 o.write('%s="%s"\n' % (varExpanded, alter))
235 return 0
236
237def emit_env(o=sys.__stdout__, d = init(), all=False):
238 """Emits all items in the data store in a format such that it can be sourced by a shell."""
239
240 isfunc = lambda key: bool(d.getVarFlag(key, "func"))
241 keys = sorted((key for key in d.keys() if not key.startswith("__")), key=isfunc)
242 grouped = groupby(keys, isfunc)
243 for isfunc, keys in grouped:
244 for key in keys:
245 emit_var(key, o, d, all and not isfunc) and o.write('\n')
246
247def exported_keys(d):
248 return (key for key in d.keys() if not key.startswith('__') and
249 d.getVarFlag(key, 'export') and
250 not d.getVarFlag(key, 'unexport'))
251
252def exported_vars(d):
253 for key in exported_keys(d):
254 try:
255 value = d.getVar(key, True)
256 except Exception:
257 pass
258
259 if value is not None:
260 yield key, str(value)
261
262def emit_func(func, o=sys.__stdout__, d = init()):
263 """Emits all items in the data store in a format such that it can be sourced by a shell."""
264
265 keys = (key for key in d.keys() if not key.startswith("__") and not d.getVarFlag(key, "func"))
266 for key in keys:
267 emit_var(key, o, d, False) and o.write('\n')
268
269 emit_var(func, o, d, False) and o.write('\n')
270 newdeps = bb.codeparser.ShellParser(func, logger).parse_shell(d.getVar(func, True))
271 newdeps |= set((d.getVarFlag(func, "vardeps", True) or "").split())
272 seen = set()
273 while newdeps:
274 deps = newdeps
275 seen |= deps
276 newdeps = set()
277 for dep in deps:
278 if d.getVarFlag(dep, "func"):
279 emit_var(dep, o, d, False) and o.write('\n')
280 newdeps |= bb.codeparser.ShellParser(dep, logger).parse_shell(d.getVar(dep, True))
281 newdeps |= set((d.getVarFlag(dep, "vardeps", True) or "").split())
282 newdeps -= seen
283
284def update_data(d):
285 """Performs final steps upon the datastore, including application of overrides"""
286 d.finalize(parent = True)
287
288def build_dependencies(key, keys, shelldeps, varflagsexcl, d):
289 deps = set()
290 try:
291 if key[-1] == ']':
292 vf = key[:-1].split('[')
293 value = d.getVarFlag(vf[0], vf[1], False)
294 parser = d.expandWithRefs(value, key)
295 deps |= parser.references
296 deps = deps | (keys & parser.execs)
297 return deps, value
298 varflags = d.getVarFlags(key, ["vardeps", "vardepvalue", "vardepsexclude"]) or {}
299 vardeps = varflags.get("vardeps")
300 value = d.getVar(key, False)
301
302 if "vardepvalue" in varflags:
303 value = varflags.get("vardepvalue")
304 elif varflags.get("func"):
305 if varflags.get("python"):
306 parsedvar = d.expandWithRefs(value, key)
307 parser = bb.codeparser.PythonParser(key, logger)
308 if parsedvar.value and "\t" in parsedvar.value:
309 logger.warn("Variable %s contains tabs, please remove these (%s)" % (key, d.getVar("FILE", True)))
310 parser.parse_python(parsedvar.value)
311 deps = deps | parser.references
312 else:
313 parsedvar = d.expandWithRefs(value, key)
314 parser = bb.codeparser.ShellParser(key, logger)
315 parser.parse_shell(parsedvar.value)
316 deps = deps | shelldeps
317 if vardeps is None:
318 parser.log.flush()
319 deps = deps | parsedvar.references
320 deps = deps | (keys & parser.execs) | (keys & parsedvar.execs)
321 else:
322 parser = d.expandWithRefs(value, key)
323 deps |= parser.references
324 deps = deps | (keys & parser.execs)
325
326 # Add varflags, assuming an exclusion list is set
327 if varflagsexcl:
328 varfdeps = []
329 for f in varflags:
330 if f not in varflagsexcl:
331 varfdeps.append('%s[%s]' % (key, f))
332 if varfdeps:
333 deps |= set(varfdeps)
334
335 deps |= set((vardeps or "").split())
336 deps -= set(varflags.get("vardepsexclude", "").split())
337 except Exception as e:
338 raise bb.data_smart.ExpansionError(key, None, e)
339 return deps, value
340 #bb.note("Variable %s references %s and calls %s" % (key, str(deps), str(execs)))
341 #d.setVarFlag(key, "vardeps", deps)
342
343def generate_dependencies(d):
344
345 keys = set(key for key in d if not key.startswith("__"))
346 shelldeps = set(key for key in d.getVar("__exportlist", False) if d.getVarFlag(key, "export") and not d.getVarFlag(key, "unexport"))
347 varflagsexcl = d.getVar('BB_SIGNATURE_EXCLUDE_FLAGS', True)
348
349 deps = {}
350 values = {}
351
352 tasklist = d.getVar('__BBTASKS') or []
353 for task in tasklist:
354 deps[task], values[task] = build_dependencies(task, keys, shelldeps, varflagsexcl, d)
355 newdeps = deps[task]
356 seen = set()
357 while newdeps:
358 nextdeps = newdeps
359 seen |= nextdeps
360 newdeps = set()
361 for dep in nextdeps:
362 if dep not in deps:
363 deps[dep], values[dep] = build_dependencies(dep, keys, shelldeps, varflagsexcl, d)
364 newdeps |= deps[dep]
365 newdeps -= seen
366 #print "For %s: %s" % (task, str(deps[task]))
367 return tasklist, deps, values
368
369def inherits_class(klass, d):
370 val = getVar('__inherit_cache', d) or []
371 needle = os.path.join('classes', '%s.bbclass' % klass)
372 for v in val:
373 if v.endswith(needle):
374 return True
375 return False
diff --git a/bitbake/lib/bb/data_smart.py b/bitbake/lib/bb/data_smart.py
new file mode 100644
index 0000000000..a1cbaba62b
--- /dev/null
+++ b/bitbake/lib/bb/data_smart.py
@@ -0,0 +1,794 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake Smart Dictionary Implementation
5
6Functions for interacting with the data structure used by the
7BitBake build tools.
8
9"""
10
11# Copyright (C) 2003, 2004 Chris Larson
12# Copyright (C) 2004, 2005 Seb Frankengul
13# Copyright (C) 2005, 2006 Holger Hans Peter Freyther
14# Copyright (C) 2005 Uli Luckas
15# Copyright (C) 2005 ROAD GmbH
16#
17# This program is free software; you can redistribute it and/or modify
18# it under the terms of the GNU General Public License version 2 as
19# published by the Free Software Foundation.
20#
21# This program is distributed in the hope that it will be useful,
22# but WITHOUT ANY WARRANTY; without even the implied warranty of
23# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24# GNU General Public License for more details.
25#
26# You should have received a copy of the GNU General Public License along
27# with this program; if not, write to the Free Software Foundation, Inc.,
28# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
29# Based on functions from the base bb module, Copyright 2003 Holger Schurig
30
31import copy, re, sys, traceback
32from collections import MutableMapping
33import logging
34import hashlib
35import bb, bb.codeparser
36from bb import utils
37from bb.COW import COWDictBase
38
39logger = logging.getLogger("BitBake.Data")
40
41__setvar_keyword__ = ["_append", "_prepend", "_remove"]
42__setvar_regexp__ = re.compile('(?P<base>.*?)(?P<keyword>_append|_prepend|_remove)(_(?P<add>.*))?$')
43__expand_var_regexp__ = re.compile(r"\${[^{}@\n\t ]+}")
44__expand_python_regexp__ = re.compile(r"\${@.+?}")
45
46def infer_caller_details(loginfo, parent = False, varval = True):
47 """Save the caller the trouble of specifying everything."""
48 # Save effort.
49 if 'ignore' in loginfo and loginfo['ignore']:
50 return
51 # If nothing was provided, mark this as possibly unneeded.
52 if not loginfo:
53 loginfo['ignore'] = True
54 return
55 # Infer caller's likely values for variable (var) and value (value),
56 # to reduce clutter in the rest of the code.
57 if varval and ('variable' not in loginfo or 'detail' not in loginfo):
58 try:
59 raise Exception
60 except Exception:
61 tb = sys.exc_info()[2]
62 if parent:
63 above = tb.tb_frame.f_back.f_back
64 else:
65 above = tb.tb_frame.f_back
66 lcls = above.f_locals.items()
67 for k, v in lcls:
68 if k == 'value' and 'detail' not in loginfo:
69 loginfo['detail'] = v
70 if k == 'var' and 'variable' not in loginfo:
71 loginfo['variable'] = v
72 # Infer file/line/function from traceback
73 if 'file' not in loginfo:
74 depth = 3
75 if parent:
76 depth = 4
77 file, line, func, text = traceback.extract_stack(limit = depth)[0]
78 loginfo['file'] = file
79 loginfo['line'] = line
80 if func not in loginfo:
81 loginfo['func'] = func
82
83class VariableParse:
84 def __init__(self, varname, d, val = None):
85 self.varname = varname
86 self.d = d
87 self.value = val
88
89 self.references = set()
90 self.execs = set()
91
92 def var_sub(self, match):
93 key = match.group()[2:-1]
94 if self.varname and key:
95 if self.varname == key:
96 raise Exception("variable %s references itself!" % self.varname)
97 if key in self.d.expand_cache:
98 varparse = self.d.expand_cache[key]
99 var = varparse.value
100 else:
101 var = self.d.getVar(key, True)
102 self.references.add(key)
103 if var is not None:
104 return var
105 else:
106 return match.group()
107
108 def python_sub(self, match):
109 code = match.group()[3:-1]
110 codeobj = compile(code.strip(), self.varname or "<expansion>", "eval")
111
112 parser = bb.codeparser.PythonParser(self.varname, logger)
113 parser.parse_python(code)
114 if self.varname:
115 vardeps = self.d.getVarFlag(self.varname, "vardeps", True)
116 if vardeps is None:
117 parser.log.flush()
118 else:
119 parser.log.flush()
120 self.references |= parser.references
121 self.execs |= parser.execs
122
123 value = utils.better_eval(codeobj, DataContext(self.d))
124 return str(value)
125
126
127class DataContext(dict):
128 def __init__(self, metadata, **kwargs):
129 self.metadata = metadata
130 dict.__init__(self, **kwargs)
131 self['d'] = metadata
132
133 def __missing__(self, key):
134 value = self.metadata.getVar(key, True)
135 if value is None or self.metadata.getVarFlag(key, 'func'):
136 raise KeyError(key)
137 else:
138 return value
139
140class ExpansionError(Exception):
141 def __init__(self, varname, expression, exception):
142 self.expression = expression
143 self.variablename = varname
144 self.exception = exception
145 if varname:
146 if expression:
147 self.msg = "Failure expanding variable %s, expression was %s which triggered exception %s: %s" % (varname, expression, type(exception).__name__, exception)
148 else:
149 self.msg = "Failure expanding variable %s: %s: %s" % (varname, type(exception).__name__, exception)
150 else:
151 self.msg = "Failure expanding expression %s which triggered exception %s: %s" % (expression, type(exception).__name__, exception)
152 Exception.__init__(self, self.msg)
153 self.args = (varname, expression, exception)
154 def __str__(self):
155 return self.msg
156
157class IncludeHistory(object):
158 def __init__(self, parent = None, filename = '[TOP LEVEL]'):
159 self.parent = parent
160 self.filename = filename
161 self.children = []
162 self.current = self
163
164 def copy(self):
165 new = IncludeHistory(self.parent, self.filename)
166 for c in self.children:
167 new.children.append(c)
168 return new
169
170 def include(self, filename):
171 newfile = IncludeHistory(self.current, filename)
172 self.current.children.append(newfile)
173 self.current = newfile
174 return self
175
176 def __enter__(self):
177 pass
178
179 def __exit__(self, a, b, c):
180 if self.current.parent:
181 self.current = self.current.parent
182 else:
183 bb.warn("Include log: Tried to finish '%s' at top level." % filename)
184 return False
185
186 def emit(self, o, level = 0):
187 """Emit an include history file, and its children."""
188 if level:
189 spaces = " " * (level - 1)
190 o.write("# %s%s" % (spaces, self.filename))
191 if len(self.children) > 0:
192 o.write(" includes:")
193 else:
194 o.write("#\n# INCLUDE HISTORY:\n#")
195 level = level + 1
196 for child in self.children:
197 o.write("\n")
198 child.emit(o, level)
199
200class VariableHistory(object):
201 def __init__(self, dataroot):
202 self.dataroot = dataroot
203 self.variables = COWDictBase.copy()
204
205 def copy(self):
206 new = VariableHistory(self.dataroot)
207 new.variables = self.variables.copy()
208 return new
209
210 def record(self, *kwonly, **loginfo):
211 if not self.dataroot._tracking:
212 return
213 if len(kwonly) > 0:
214 raise TypeError
215 infer_caller_details(loginfo, parent = True)
216 if 'ignore' in loginfo and loginfo['ignore']:
217 return
218 if 'op' not in loginfo or not loginfo['op']:
219 loginfo['op'] = 'set'
220 if 'detail' in loginfo:
221 loginfo['detail'] = str(loginfo['detail'])
222 if 'variable' not in loginfo or 'file' not in loginfo:
223 raise ValueError("record() missing variable or file.")
224 var = loginfo['variable']
225
226 if var not in self.variables:
227 self.variables[var] = []
228 self.variables[var].append(loginfo.copy())
229
230 def variable(self, var):
231 if var in self.variables:
232 return self.variables[var]
233 else:
234 return []
235
236 def emit(self, var, oval, val, o):
237 history = self.variable(var)
238 commentVal = re.sub('\n', '\n#', str(oval))
239 if history:
240 if len(history) == 1:
241 o.write("#\n# $%s\n" % var)
242 else:
243 o.write("#\n# $%s [%d operations]\n" % (var, len(history)))
244 for event in history:
245 # o.write("# %s\n" % str(event))
246 if 'func' in event:
247 # If we have a function listed, this is internal
248 # code, not an operation in a config file, and the
249 # full path is distracting.
250 event['file'] = re.sub('.*/', '', event['file'])
251 display_func = ' [%s]' % event['func']
252 else:
253 display_func = ''
254 if 'flag' in event:
255 flag = '[%s] ' % (event['flag'])
256 else:
257 flag = ''
258 o.write("# %s %s:%s%s\n# %s\"%s\"\n" % (event['op'], event['file'], event['line'], display_func, flag, re.sub('\n', '\n# ', event['detail'])))
259 if len(history) > 1:
260 o.write("# computed:\n")
261 o.write('# "%s"\n' % (commentVal))
262 else:
263 o.write("#\n# $%s\n# [no history recorded]\n#\n" % var)
264 o.write('# "%s"\n' % (commentVal))
265
266 def get_variable_files(self, var):
267 """Get the files where operations are made on a variable"""
268 var_history = self.variable(var)
269 files = []
270 for event in var_history:
271 files.append(event['file'])
272 return files
273
274 def get_variable_lines(self, var, f):
275 """Get the line where a operation is made on a variable in file f"""
276 var_history = self.variable(var)
277 lines = []
278 for event in var_history:
279 if f== event['file']:
280 line = event['line']
281 lines.append(line)
282 return lines
283
284 def del_var_history(self, var, f=None, line=None):
285 """If file f and line are not given, the entire history of var is deleted"""
286 if var in self.variables:
287 if f and line:
288 self.variables[var] = [ x for x in self.variables[var] if x['file']!=f and x['line']!=line]
289 else:
290 self.variables[var] = []
291
292class DataSmart(MutableMapping):
293 def __init__(self, special = COWDictBase.copy(), seen = COWDictBase.copy() ):
294 self.dict = {}
295
296 self.inchistory = IncludeHistory()
297 self.varhistory = VariableHistory(self)
298 self._tracking = False
299
300 # cookie monster tribute
301 self._special_values = special
302 self._seen_overrides = seen
303
304 self.expand_cache = {}
305
306 def enableTracking(self):
307 self._tracking = True
308
309 def disableTracking(self):
310 self._tracking = False
311
312 def expandWithRefs(self, s, varname):
313
314 if not isinstance(s, basestring): # sanity check
315 return VariableParse(varname, self, s)
316
317 if varname and varname in self.expand_cache:
318 return self.expand_cache[varname]
319
320 varparse = VariableParse(varname, self)
321
322 while s.find('${') != -1:
323 olds = s
324 try:
325 s = __expand_var_regexp__.sub(varparse.var_sub, s)
326 s = __expand_python_regexp__.sub(varparse.python_sub, s)
327 if s == olds:
328 break
329 except ExpansionError:
330 raise
331 except bb.parse.SkipPackage:
332 raise
333 except Exception as exc:
334 raise ExpansionError(varname, s, exc)
335
336 varparse.value = s
337
338 if varname:
339 self.expand_cache[varname] = varparse
340
341 return varparse
342
343 def expand(self, s, varname = None):
344 return self.expandWithRefs(s, varname).value
345
346
347 def finalize(self, parent = False):
348 """Performs final steps upon the datastore, including application of overrides"""
349
350 overrides = (self.getVar("OVERRIDES", True) or "").split(":") or []
351 finalize_caller = {
352 'op': 'finalize',
353 }
354 infer_caller_details(finalize_caller, parent = parent, varval = False)
355
356 #
357 # Well let us see what breaks here. We used to iterate
358 # over each variable and apply the override and then
359 # do the line expanding.
360 # If we have bad luck - which we will have - the keys
361 # where in some order that is so important for this
362 # method which we don't have anymore.
363 # Anyway we will fix that and write test cases this
364 # time.
365
366 #
367 # First we apply all overrides
368 # Then we will handle _append and _prepend and store the _remove
369 # information for later.
370 #
371
372 # We only want to report finalization once per variable overridden.
373 finalizes_reported = {}
374
375 for o in overrides:
376 # calculate '_'+override
377 l = len(o) + 1
378
379 # see if one should even try
380 if o not in self._seen_overrides:
381 continue
382
383 vars = self._seen_overrides[o].copy()
384 for var in vars:
385 name = var[:-l]
386 try:
387 # Report only once, even if multiple changes.
388 if name not in finalizes_reported:
389 finalizes_reported[name] = True
390 finalize_caller['variable'] = name
391 finalize_caller['detail'] = 'was: ' + str(self.getVar(name, False))
392 self.varhistory.record(**finalize_caller)
393 # Copy history of the override over.
394 for event in self.varhistory.variable(var):
395 loginfo = event.copy()
396 loginfo['variable'] = name
397 loginfo['op'] = 'override[%s]:%s' % (o, loginfo['op'])
398 self.varhistory.record(**loginfo)
399 self.setVar(name, self.getVar(var, False), op = 'finalize', file = 'override[%s]' % o, line = '')
400 self.delVar(var)
401 except Exception:
402 logger.info("Untracked delVar")
403
404 # now on to the appends and prepends, and stashing the removes
405 for op in __setvar_keyword__:
406 if op in self._special_values:
407 appends = self._special_values[op] or []
408 for append in appends:
409 keep = []
410 for (a, o) in self.getVarFlag(append, op) or []:
411 match = True
412 if o:
413 for o2 in o.split("_"):
414 if not o2 in overrides:
415 match = False
416 if not match:
417 keep.append((a ,o))
418 continue
419
420 if op == "_append":
421 sval = self.getVar(append, False) or ""
422 sval += a
423 self.setVar(append, sval)
424 elif op == "_prepend":
425 sval = a + (self.getVar(append, False) or "")
426 self.setVar(append, sval)
427 elif op == "_remove":
428 removes = self.getVarFlag(append, "_removeactive", False) or []
429 removes.extend(a.split())
430 self.setVarFlag(append, "_removeactive", removes, ignore=True)
431
432 # We save overrides that may be applied at some later stage
433 if keep:
434 self.setVarFlag(append, op, keep, ignore=True)
435 else:
436 self.delVarFlag(append, op, ignore=True)
437
438 def initVar(self, var):
439 self.expand_cache = {}
440 if not var in self.dict:
441 self.dict[var] = {}
442
443 def _findVar(self, var):
444 dest = self.dict
445 while dest:
446 if var in dest:
447 return dest[var]
448
449 if "_data" not in dest:
450 break
451 dest = dest["_data"]
452
453 def _makeShadowCopy(self, var):
454 if var in self.dict:
455 return
456
457 local_var = self._findVar(var)
458
459 if local_var:
460 self.dict[var] = copy.copy(local_var)
461 else:
462 self.initVar(var)
463
464
465 def setVar(self, var, value, **loginfo):
466 #print("var=" + str(var) + " val=" + str(value))
467 if 'op' not in loginfo:
468 loginfo['op'] = "set"
469 self.expand_cache = {}
470 match = __setvar_regexp__.match(var)
471 if match and match.group("keyword") in __setvar_keyword__:
472 base = match.group('base')
473 keyword = match.group("keyword")
474 override = match.group('add')
475 l = self.getVarFlag(base, keyword) or []
476 l.append([value, override])
477 self.setVarFlag(base, keyword, l, ignore=True)
478 # And cause that to be recorded:
479 loginfo['detail'] = value
480 loginfo['variable'] = base
481 if override:
482 loginfo['op'] = '%s[%s]' % (keyword, override)
483 else:
484 loginfo['op'] = keyword
485 self.varhistory.record(**loginfo)
486 # todo make sure keyword is not __doc__ or __module__
487 # pay the cookie monster
488 try:
489 self._special_values[keyword].add(base)
490 except KeyError:
491 self._special_values[keyword] = set()
492 self._special_values[keyword].add(base)
493
494 return
495
496 if not var in self.dict:
497 self._makeShadowCopy(var)
498
499 # more cookies for the cookie monster
500 if '_' in var:
501 self._setvar_update_overrides(var)
502
503 # setting var
504 self.dict[var]["_content"] = value
505 self.varhistory.record(**loginfo)
506
507 def _setvar_update_overrides(self, var):
508 # aka pay the cookie monster
509 override = var[var.rfind('_')+1:]
510 if len(override) > 0:
511 if override not in self._seen_overrides:
512 self._seen_overrides[override] = set()
513 self._seen_overrides[override].add( var )
514
515 def getVar(self, var, expand=False, noweakdefault=False):
516 return self.getVarFlag(var, "_content", expand, noweakdefault)
517
518 def renameVar(self, key, newkey, **loginfo):
519 """
520 Rename the variable key to newkey
521 """
522 val = self.getVar(key, 0)
523 if val is not None:
524 loginfo['variable'] = newkey
525 loginfo['op'] = 'rename from %s' % key
526 loginfo['detail'] = val
527 self.varhistory.record(**loginfo)
528 self.setVar(newkey, val, ignore=True)
529
530 for i in (__setvar_keyword__):
531 src = self.getVarFlag(key, i)
532 if src is None:
533 continue
534
535 dest = self.getVarFlag(newkey, i) or []
536 dest.extend(src)
537 self.setVarFlag(newkey, i, dest, ignore=True)
538
539 if i in self._special_values and key in self._special_values[i]:
540 self._special_values[i].remove(key)
541 self._special_values[i].add(newkey)
542
543 loginfo['variable'] = key
544 loginfo['op'] = 'rename (to)'
545 loginfo['detail'] = newkey
546 self.varhistory.record(**loginfo)
547 self.delVar(key, ignore=True)
548
549 def appendVar(self, var, value, **loginfo):
550 loginfo['op'] = 'append'
551 self.varhistory.record(**loginfo)
552 newvalue = (self.getVar(var, False) or "") + value
553 self.setVar(var, newvalue, ignore=True)
554
555 def prependVar(self, var, value, **loginfo):
556 loginfo['op'] = 'prepend'
557 self.varhistory.record(**loginfo)
558 newvalue = value + (self.getVar(var, False) or "")
559 self.setVar(var, newvalue, ignore=True)
560
561 def delVar(self, var, **loginfo):
562 loginfo['detail'] = ""
563 loginfo['op'] = 'del'
564 self.varhistory.record(**loginfo)
565 self.expand_cache = {}
566 self.dict[var] = {}
567 if '_' in var:
568 override = var[var.rfind('_')+1:]
569 if override and override in self._seen_overrides and var in self._seen_overrides[override]:
570 self._seen_overrides[override].remove(var)
571
572 def setVarFlag(self, var, flag, value, **loginfo):
573 if 'op' not in loginfo:
574 loginfo['op'] = "set"
575 loginfo['flag'] = flag
576 self.varhistory.record(**loginfo)
577 if not var in self.dict:
578 self._makeShadowCopy(var)
579 self.dict[var][flag] = value
580
581 if flag == "defaultval" and '_' in var:
582 self._setvar_update_overrides(var)
583
584 if flag == "unexport" or flag == "export":
585 if not "__exportlist" in self.dict:
586 self._makeShadowCopy("__exportlist")
587 if not "_content" in self.dict["__exportlist"]:
588 self.dict["__exportlist"]["_content"] = set()
589 self.dict["__exportlist"]["_content"].add(var)
590
591 def getVarFlag(self, var, flag, expand=False, noweakdefault=False):
592 local_var = self._findVar(var)
593 value = None
594 if local_var is not None:
595 if flag in local_var:
596 value = copy.copy(local_var[flag])
597 elif flag == "_content" and "defaultval" in local_var and not noweakdefault:
598 value = copy.copy(local_var["defaultval"])
599 if expand and value:
600 # Only getvar (flag == _content) hits the expand cache
601 cachename = None
602 if flag == "_content":
603 cachename = var
604 else:
605 cachename = var + "[" + flag + "]"
606 value = self.expand(value, cachename)
607 if value is not None and flag == "_content" and local_var is not None and "_removeactive" in local_var:
608 filtered = filter(lambda v: v not in local_var["_removeactive"],
609 value.split(" "))
610 value = " ".join(filtered)
611 return value
612
613 def delVarFlag(self, var, flag, **loginfo):
614 local_var = self._findVar(var)
615 if not local_var:
616 return
617 if not var in self.dict:
618 self._makeShadowCopy(var)
619
620 if var in self.dict and flag in self.dict[var]:
621 loginfo['detail'] = ""
622 loginfo['op'] = 'delFlag'
623 loginfo['flag'] = flag
624 self.varhistory.record(**loginfo)
625
626 del self.dict[var][flag]
627
628 def appendVarFlag(self, var, flag, value, **loginfo):
629 loginfo['op'] = 'append'
630 loginfo['flag'] = flag
631 self.varhistory.record(**loginfo)
632 newvalue = (self.getVarFlag(var, flag, False) or "") + value
633 self.setVarFlag(var, flag, newvalue, ignore=True)
634
635 def prependVarFlag(self, var, flag, value, **loginfo):
636 loginfo['op'] = 'prepend'
637 loginfo['flag'] = flag
638 self.varhistory.record(**loginfo)
639 newvalue = value + (self.getVarFlag(var, flag, False) or "")
640 self.setVarFlag(var, flag, newvalue, ignore=True)
641
642 def setVarFlags(self, var, flags, **loginfo):
643 infer_caller_details(loginfo)
644 if not var in self.dict:
645 self._makeShadowCopy(var)
646
647 for i in flags:
648 if i == "_content":
649 continue
650 loginfo['flag'] = i
651 loginfo['detail'] = flags[i]
652 self.varhistory.record(**loginfo)
653 self.dict[var][i] = flags[i]
654
655 def getVarFlags(self, var, expand = False, internalflags=False):
656 local_var = self._findVar(var)
657 flags = {}
658
659 if local_var:
660 for i in local_var:
661 if i.startswith("_") and not internalflags:
662 continue
663 flags[i] = local_var[i]
664 if expand and i in expand:
665 flags[i] = self.expand(flags[i], var + "[" + i + "]")
666 if len(flags) == 0:
667 return None
668 return flags
669
670
671 def delVarFlags(self, var, **loginfo):
672 if not var in self.dict:
673 self._makeShadowCopy(var)
674
675 if var in self.dict:
676 content = None
677
678 loginfo['op'] = 'delete flags'
679 self.varhistory.record(**loginfo)
680
681 # try to save the content
682 if "_content" in self.dict[var]:
683 content = self.dict[var]["_content"]
684 self.dict[var] = {}
685 self.dict[var]["_content"] = content
686 else:
687 del self.dict[var]
688
689
690 def createCopy(self):
691 """
692 Create a copy of self by setting _data to self
693 """
694 # we really want this to be a DataSmart...
695 data = DataSmart(seen=self._seen_overrides.copy(), special=self._special_values.copy())
696 data.dict["_data"] = self.dict
697 data.varhistory = self.varhistory.copy()
698 data.varhistory.datasmart = data
699 data.inchistory = self.inchistory.copy()
700
701 data._tracking = self._tracking
702
703 return data
704
705 def expandVarref(self, variable, parents=False):
706 """Find all references to variable in the data and expand it
707 in place, optionally descending to parent datastores."""
708
709 if parents:
710 keys = iter(self)
711 else:
712 keys = self.localkeys()
713
714 ref = '${%s}' % variable
715 value = self.getVar(variable, False)
716 for key in keys:
717 referrervalue = self.getVar(key, False)
718 if referrervalue and ref in referrervalue:
719 self.setVar(key, referrervalue.replace(ref, value))
720
721 def localkeys(self):
722 for key in self.dict:
723 if key != '_data':
724 yield key
725
726 def __iter__(self):
727 def keylist(d):
728 klist = set()
729 for key in d:
730 if key == "_data":
731 continue
732 if not d[key]:
733 continue
734 klist.add(key)
735
736 if "_data" in d:
737 klist |= keylist(d["_data"])
738
739 return klist
740
741 for k in keylist(self.dict):
742 yield k
743
744 def __len__(self):
745 return len(frozenset(self))
746
747 def __getitem__(self, item):
748 value = self.getVar(item, False)
749 if value is None:
750 raise KeyError(item)
751 else:
752 return value
753
754 def __setitem__(self, var, value):
755 self.setVar(var, value)
756
757 def __delitem__(self, var):
758 self.delVar(var)
759
760 def get_hash(self):
761 data = {}
762 d = self.createCopy()
763 bb.data.expandKeys(d)
764 bb.data.update_data(d)
765
766 config_whitelist = set((d.getVar("BB_HASHCONFIG_WHITELIST", True) or "").split())
767 keys = set(key for key in iter(d) if not key.startswith("__"))
768 for key in keys:
769 if key in config_whitelist:
770 continue
771
772 value = d.getVar(key, False) or ""
773 data.update({key:value})
774
775 varflags = d.getVarFlags(key, internalflags = True)
776 if not varflags:
777 continue
778 for f in varflags:
779 if f == "_content":
780 continue
781 data.update({'%s[%s]' % (key, f):varflags[f]})
782
783 for key in ["__BBTASKS", "__BBANONFUNCS", "__BBHANDLERS"]:
784 bb_list = d.getVar(key, False) or []
785 bb_list.sort()
786 data.update({key:str(bb_list)})
787
788 if key == "__BBANONFUNCS":
789 for i in bb_list:
790 value = d.getVar(i, True) or ""
791 data.update({i:value})
792
793 data_str = str([(k, data[k]) for k in sorted(data.keys())])
794 return hashlib.md5(data_str).hexdigest()
diff --git a/bitbake/lib/bb/event.py b/bitbake/lib/bb/event.py
new file mode 100644
index 0000000000..10eae5fde8
--- /dev/null
+++ b/bitbake/lib/bb/event.py
@@ -0,0 +1,635 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Event' implementation
5
6Classes and functions for manipulating 'events' in the
7BitBake build tools.
8"""
9
10# Copyright (C) 2003, 2004 Chris Larson
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License version 2 as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along
22# with this program; if not, write to the Free Software Foundation, Inc.,
23# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24
25import os, sys
26import warnings
27try:
28 import cPickle as pickle
29except ImportError:
30 import pickle
31import logging
32import atexit
33import traceback
34import bb.utils
35import bb.compat
36import bb.exceptions
37
38# This is the pid for which we should generate the event. This is set when
39# the runqueue forks off.
40worker_pid = 0
41worker_fire = None
42
43logger = logging.getLogger('BitBake.Event')
44
45class Event(object):
46 """Base class for events"""
47
48 def __init__(self):
49 self.pid = worker_pid
50
51Registered = 10
52AlreadyRegistered = 14
53
54def get_class_handlers():
55 return _handlers
56
57def set_class_handlers(h):
58 _handlers = h
59
60def clean_class_handlers():
61 return bb.compat.OrderedDict()
62
63# Internal
64_handlers = clean_class_handlers()
65_ui_handlers = {}
66_ui_logfilters = {}
67_ui_handler_seq = 0
68_event_handler_map = {}
69_catchall_handlers = {}
70
71def execute_handler(name, handler, event, d):
72 event.data = d
73 try:
74 ret = handler(event)
75 except bb.parse.SkipPackage:
76 raise
77 except Exception:
78 etype, value, tb = sys.exc_info()
79 logger.error("Execution of event handler '%s' failed" % name,
80 exc_info=(etype, value, tb.tb_next))
81 raise
82 except SystemExit as exc:
83 if exc.code != 0:
84 logger.error("Execution of event handler '%s' failed" % name)
85 raise
86 finally:
87 del event.data
88
89def fire_class_handlers(event, d):
90 if isinstance(event, logging.LogRecord):
91 return
92
93 eid = str(event.__class__)[8:-2]
94 evt_hmap = _event_handler_map.get(eid, {})
95 for name, handler in _handlers.iteritems():
96 if name in _catchall_handlers or name in evt_hmap:
97 try:
98 execute_handler(name, handler, event, d)
99 except Exception:
100 continue
101
102ui_queue = []
103@atexit.register
104def print_ui_queue():
105 """If we're exiting before a UI has been spawned, display any queued
106 LogRecords to the console."""
107 logger = logging.getLogger("BitBake")
108 if not _ui_handlers:
109 from bb.msg import BBLogFormatter
110 console = logging.StreamHandler(sys.stdout)
111 console.setFormatter(BBLogFormatter("%(levelname)s: %(message)s"))
112 logger.handlers = [console]
113
114 # First check to see if we have any proper messages
115 msgprint = False
116 for event in ui_queue:
117 if isinstance(event, logging.LogRecord):
118 if event.levelno > logging.DEBUG:
119 logger.handle(event)
120 msgprint = True
121 if msgprint:
122 return
123
124 # Nope, so just print all of the messages we have (including debug messages)
125 for event in ui_queue:
126 if isinstance(event, logging.LogRecord):
127 logger.handle(event)
128
129def fire_ui_handlers(event, d):
130 if not _ui_handlers:
131 # No UI handlers registered yet, queue up the messages
132 ui_queue.append(event)
133 return
134
135 errors = []
136 for h in _ui_handlers:
137 #print "Sending event %s" % event
138 try:
139 if not _ui_logfilters[h].filter(event):
140 continue
141 # We use pickle here since it better handles object instances
142 # which xmlrpc's marshaller does not. Events *must* be serializable
143 # by pickle.
144 if hasattr(_ui_handlers[h].event, "sendpickle"):
145 _ui_handlers[h].event.sendpickle((pickle.dumps(event)))
146 else:
147 _ui_handlers[h].event.send(event)
148 except:
149 errors.append(h)
150 for h in errors:
151 del _ui_handlers[h]
152
153def fire(event, d):
154 """Fire off an Event"""
155
156 # We can fire class handlers in the worker process context and this is
157 # desired so they get the task based datastore.
158 # UI handlers need to be fired in the server context so we defer this. They
159 # don't have a datastore so the datastore context isn't a problem.
160
161 fire_class_handlers(event, d)
162 if worker_fire:
163 worker_fire(event, d)
164 else:
165 fire_ui_handlers(event, d)
166
167def fire_from_worker(event, d):
168 fire_ui_handlers(event, d)
169
170noop = lambda _: None
171def register(name, handler, mask=[]):
172 """Register an Event handler"""
173
174 # already registered
175 if name in _handlers:
176 return AlreadyRegistered
177
178 if handler is not None:
179 # handle string containing python code
180 if isinstance(handler, basestring):
181 tmp = "def %s(e):\n%s" % (name, handler)
182 try:
183 code = compile(tmp, "%s(e)" % name, "exec")
184 except SyntaxError:
185 logger.error("Unable to register event handler '%s':\n%s", name,
186 ''.join(traceback.format_exc(limit=0)))
187 _handlers[name] = noop
188 return
189 env = {}
190 bb.utils.better_exec(code, env)
191 func = bb.utils.better_eval(name, env)
192 _handlers[name] = func
193 else:
194 _handlers[name] = handler
195
196 if not mask or '*' in mask:
197 _catchall_handlers[name] = True
198 else:
199 for m in mask:
200 if _event_handler_map.get(m, None) is None:
201 _event_handler_map[m] = {}
202 _event_handler_map[m][name] = True
203
204 return Registered
205
206def remove(name, handler):
207 """Remove an Event handler"""
208 _handlers.pop(name)
209
210def register_UIHhandler(handler):
211 bb.event._ui_handler_seq = bb.event._ui_handler_seq + 1
212 _ui_handlers[_ui_handler_seq] = handler
213 level, debug_domains = bb.msg.constructLogOptions()
214 _ui_logfilters[_ui_handler_seq] = UIEventFilter(level, debug_domains)
215 return _ui_handler_seq
216
217def unregister_UIHhandler(handlerNum):
218 if handlerNum in _ui_handlers:
219 del _ui_handlers[handlerNum]
220 return
221
222# Class to allow filtering of events and specific filtering of LogRecords *before* we put them over the IPC
223class UIEventFilter(object):
224 def __init__(self, level, debug_domains):
225 self.update(None, level, debug_domains)
226
227 def update(self, eventmask, level, debug_domains):
228 self.eventmask = eventmask
229 self.stdlevel = level
230 self.debug_domains = debug_domains
231
232 def filter(self, event):
233 if isinstance(event, logging.LogRecord):
234 if event.levelno >= self.stdlevel:
235 return True
236 if event.name in self.debug_domains and event.levelno >= self.debug_domains[event.name]:
237 return True
238 return False
239 eid = str(event.__class__)[8:-2]
240 if self.eventmask and eid not in self.eventmask:
241 return False
242 return True
243
244def set_UIHmask(handlerNum, level, debug_domains, mask):
245 if not handlerNum in _ui_handlers:
246 return False
247 if '*' in mask:
248 _ui_logfilters[handlerNum].update(None, level, debug_domains)
249 else:
250 _ui_logfilters[handlerNum].update(mask, level, debug_domains)
251 return True
252
253def getName(e):
254 """Returns the name of a class or class instance"""
255 if getattr(e, "__name__", None) == None:
256 return e.__class__.__name__
257 else:
258 return e.__name__
259
260class OperationStarted(Event):
261 """An operation has begun"""
262 def __init__(self, msg = "Operation Started"):
263 Event.__init__(self)
264 self.msg = msg
265
266class OperationCompleted(Event):
267 """An operation has completed"""
268 def __init__(self, total, msg = "Operation Completed"):
269 Event.__init__(self)
270 self.total = total
271 self.msg = msg
272
273class OperationProgress(Event):
274 """An operation is in progress"""
275 def __init__(self, current, total, msg = "Operation in Progress"):
276 Event.__init__(self)
277 self.current = current
278 self.total = total
279 self.msg = msg + ": %s/%s" % (current, total);
280
281class ConfigParsed(Event):
282 """Configuration Parsing Complete"""
283
284class RecipeEvent(Event):
285 def __init__(self, fn):
286 self.fn = fn
287 Event.__init__(self)
288
289class RecipePreFinalise(RecipeEvent):
290 """ Recipe Parsing Complete but not yet finialised"""
291
292class RecipeParsed(RecipeEvent):
293 """ Recipe Parsing Complete """
294
295class StampUpdate(Event):
296 """Trigger for any adjustment of the stamp files to happen"""
297
298 def __init__(self, targets, stampfns):
299 self._targets = targets
300 self._stampfns = stampfns
301 Event.__init__(self)
302
303 def getStampPrefix(self):
304 return self._stampfns
305
306 def getTargets(self):
307 return self._targets
308
309 stampPrefix = property(getStampPrefix)
310 targets = property(getTargets)
311
312class BuildBase(Event):
313 """Base class for bbmake run events"""
314
315 def __init__(self, n, p, failures = 0):
316 self._name = n
317 self._pkgs = p
318 Event.__init__(self)
319 self._failures = failures
320
321 def getPkgs(self):
322 return self._pkgs
323
324 def setPkgs(self, pkgs):
325 self._pkgs = pkgs
326
327 def getName(self):
328 return self._name
329
330 def setName(self, name):
331 self._name = name
332
333 def getCfg(self):
334 return self.data
335
336 def setCfg(self, cfg):
337 self.data = cfg
338
339 def getFailures(self):
340 """
341 Return the number of failed packages
342 """
343 return self._failures
344
345 pkgs = property(getPkgs, setPkgs, None, "pkgs property")
346 name = property(getName, setName, None, "name property")
347 cfg = property(getCfg, setCfg, None, "cfg property")
348
349
350
351
352
353class BuildStarted(BuildBase, OperationStarted):
354 """bbmake build run started"""
355 def __init__(self, n, p, failures = 0):
356 OperationStarted.__init__(self, "Building Started")
357 BuildBase.__init__(self, n, p, failures)
358
359class BuildCompleted(BuildBase, OperationCompleted):
360 """bbmake build run completed"""
361 def __init__(self, total, n, p, failures = 0):
362 if not failures:
363 OperationCompleted.__init__(self, total, "Building Succeeded")
364 else:
365 OperationCompleted.__init__(self, total, "Building Failed")
366 BuildBase.__init__(self, n, p, failures)
367
368class DiskFull(Event):
369 """Disk full case build aborted"""
370 def __init__(self, dev, type, freespace, mountpoint):
371 Event.__init__(self)
372 self._dev = dev
373 self._type = type
374 self._free = freespace
375 self._mountpoint = mountpoint
376
377class NoProvider(Event):
378 """No Provider for an Event"""
379
380 def __init__(self, item, runtime=False, dependees=None, reasons=[], close_matches=[]):
381 Event.__init__(self)
382 self._item = item
383 self._runtime = runtime
384 self._dependees = dependees
385 self._reasons = reasons
386 self._close_matches = close_matches
387
388 def getItem(self):
389 return self._item
390
391 def isRuntime(self):
392 return self._runtime
393
394class MultipleProviders(Event):
395 """Multiple Providers"""
396
397 def __init__(self, item, candidates, runtime = False):
398 Event.__init__(self)
399 self._item = item
400 self._candidates = candidates
401 self._is_runtime = runtime
402
403 def isRuntime(self):
404 """
405 Is this a runtime issue?
406 """
407 return self._is_runtime
408
409 def getItem(self):
410 """
411 The name for the to be build item
412 """
413 return self._item
414
415 def getCandidates(self):
416 """
417 Get the possible Candidates for a PROVIDER.
418 """
419 return self._candidates
420
421class ParseStarted(OperationStarted):
422 """Recipe parsing for the runqueue has begun"""
423 def __init__(self, total):
424 OperationStarted.__init__(self, "Recipe parsing Started")
425 self.total = total
426
427class ParseCompleted(OperationCompleted):
428 """Recipe parsing for the runqueue has completed"""
429 def __init__(self, cached, parsed, skipped, masked, virtuals, errors, total):
430 OperationCompleted.__init__(self, total, "Recipe parsing Completed")
431 self.cached = cached
432 self.parsed = parsed
433 self.skipped = skipped
434 self.virtuals = virtuals
435 self.masked = masked
436 self.errors = errors
437 self.sofar = cached + parsed
438
439class ParseProgress(OperationProgress):
440 """Recipe parsing progress"""
441 def __init__(self, current, total):
442 OperationProgress.__init__(self, current, total, "Recipe parsing")
443
444
445class CacheLoadStarted(OperationStarted):
446 """Loading of the dependency cache has begun"""
447 def __init__(self, total):
448 OperationStarted.__init__(self, "Loading cache Started")
449 self.total = total
450
451class CacheLoadProgress(OperationProgress):
452 """Cache loading progress"""
453 def __init__(self, current, total):
454 OperationProgress.__init__(self, current, total, "Loading cache")
455
456class CacheLoadCompleted(OperationCompleted):
457 """Cache loading is complete"""
458 def __init__(self, total, num_entries):
459 OperationCompleted.__init__(self, total, "Loading cache Completed")
460 self.num_entries = num_entries
461
462class TreeDataPreparationStarted(OperationStarted):
463 """Tree data preparation started"""
464 def __init__(self):
465 OperationStarted.__init__(self, "Preparing tree data Started")
466
467class TreeDataPreparationProgress(OperationProgress):
468 """Tree data preparation is in progress"""
469 def __init__(self, current, total):
470 OperationProgress.__init__(self, current, total, "Preparing tree data")
471
472class TreeDataPreparationCompleted(OperationCompleted):
473 """Tree data preparation completed"""
474 def __init__(self, total):
475 OperationCompleted.__init__(self, total, "Preparing tree data Completed")
476
477class DepTreeGenerated(Event):
478 """
479 Event when a dependency tree has been generated
480 """
481
482 def __init__(self, depgraph):
483 Event.__init__(self)
484 self._depgraph = depgraph
485
486class TargetsTreeGenerated(Event):
487 """
488 Event when a set of buildable targets has been generated
489 """
490 def __init__(self, model):
491 Event.__init__(self)
492 self._model = model
493
494class FilesMatchingFound(Event):
495 """
496 Event when a list of files matching the supplied pattern has
497 been generated
498 """
499 def __init__(self, pattern, matches):
500 Event.__init__(self)
501 self._pattern = pattern
502 self._matches = matches
503
504class CoreBaseFilesFound(Event):
505 """
506 Event when a list of appropriate config files has been generated
507 """
508 def __init__(self, paths):
509 Event.__init__(self)
510 self._paths = paths
511
512class ConfigFilesFound(Event):
513 """
514 Event when a list of appropriate config files has been generated
515 """
516 def __init__(self, variable, values):
517 Event.__init__(self)
518 self._variable = variable
519 self._values = values
520
521class ConfigFilePathFound(Event):
522 """
523 Event when a path for a config file has been found
524 """
525 def __init__(self, path):
526 Event.__init__(self)
527 self._path = path
528
529class MsgBase(Event):
530 """Base class for messages"""
531
532 def __init__(self, msg):
533 self._message = msg
534 Event.__init__(self)
535
536class MsgDebug(MsgBase):
537 """Debug Message"""
538
539class MsgNote(MsgBase):
540 """Note Message"""
541
542class MsgWarn(MsgBase):
543 """Warning Message"""
544
545class MsgError(MsgBase):
546 """Error Message"""
547
548class MsgFatal(MsgBase):
549 """Fatal Message"""
550
551class MsgPlain(MsgBase):
552 """General output"""
553
554class LogExecTTY(Event):
555 """Send event containing program to spawn on tty of the logger"""
556 def __init__(self, msg, prog, sleep_delay, retries):
557 Event.__init__(self)
558 self.msg = msg
559 self.prog = prog
560 self.sleep_delay = sleep_delay
561 self.retries = retries
562
563class LogHandler(logging.Handler):
564 """Dispatch logging messages as bitbake events"""
565
566 def emit(self, record):
567 if record.exc_info:
568 etype, value, tb = record.exc_info
569 if hasattr(tb, 'tb_next'):
570 tb = list(bb.exceptions.extract_traceback(tb, context=3))
571 record.bb_exc_info = (etype, value, tb)
572 record.exc_info = None
573 fire(record, None)
574
575 def filter(self, record):
576 record.taskpid = worker_pid
577 return True
578
579class RequestPackageInfo(Event):
580 """
581 Event to request package information
582 """
583
584class PackageInfo(Event):
585 """
586 Package information for GUI
587 """
588 def __init__(self, pkginfolist):
589 Event.__init__(self)
590 self._pkginfolist = pkginfolist
591
592class MetadataEvent(Event):
593 """
594 Generic event that target for OE-Core classes
595 to report information during asynchrous execution
596 """
597 def __init__(self, eventtype, eventdata):
598 Event.__init__(self)
599 self.type = eventtype
600 self.data = eventdata
601
602class SanityCheck(Event):
603 """
604 Event to issue sanity check
605 """
606
607class SanityCheckPassed(Event):
608 """
609 Event to indicate sanity check is passed
610 """
611
612class SanityCheckFailed(Event):
613 """
614 Event to indicate sanity check has failed
615 """
616 def __init__(self, msg, network_error=False):
617 Event.__init__(self)
618 self._msg = msg
619 self._network_error = network_error
620
621class NetworkTest(Event):
622 """
623 Event to start network test
624 """
625
626class NetworkTestPassed(Event):
627 """
628 Event to indicate network test has passed
629 """
630
631class NetworkTestFailed(Event):
632 """
633 Event to indicate network test has failed
634 """
635
diff --git a/bitbake/lib/bb/exceptions.py b/bitbake/lib/bb/exceptions.py
new file mode 100644
index 0000000000..f182c8fd62
--- /dev/null
+++ b/bitbake/lib/bb/exceptions.py
@@ -0,0 +1,91 @@
1from __future__ import absolute_import
2import inspect
3import traceback
4import bb.namedtuple_with_abc
5from collections import namedtuple
6
7
8class TracebackEntry(namedtuple.abc):
9 """Pickleable representation of a traceback entry"""
10 _fields = 'filename lineno function args code_context index'
11 _header = ' File "{0.filename}", line {0.lineno}, in {0.function}{0.args}'
12
13 def format(self, formatter=None):
14 if not self.code_context:
15 return self._header.format(self) + '\n'
16
17 formatted = [self._header.format(self) + ':\n']
18
19 for lineindex, line in enumerate(self.code_context):
20 if formatter:
21 line = formatter(line)
22
23 if lineindex == self.index:
24 formatted.append(' >%s' % line)
25 else:
26 formatted.append(' %s' % line)
27 return formatted
28
29 def __str__(self):
30 return ''.join(self.format())
31
32def _get_frame_args(frame):
33 """Get the formatted arguments and class (if available) for a frame"""
34 arginfo = inspect.getargvalues(frame)
35
36 try:
37 if not arginfo.args:
38 return '', None
39 # There have been reports from the field of python 2.6 which doesn't
40 # return a namedtuple here but simply a tuple so fallback gracefully if
41 # args isn't present.
42 except AttributeError:
43 return '', None
44
45 firstarg = arginfo.args[0]
46 if firstarg == 'self':
47 self = arginfo.locals['self']
48 cls = self.__class__.__name__
49
50 arginfo.args.pop(0)
51 del arginfo.locals['self']
52 else:
53 cls = None
54
55 formatted = inspect.formatargvalues(*arginfo)
56 return formatted, cls
57
58def extract_traceback(tb, context=1):
59 frames = inspect.getinnerframes(tb, context)
60 for frame, filename, lineno, function, code_context, index in frames:
61 formatted_args, cls = _get_frame_args(frame)
62 if cls:
63 function = '%s.%s' % (cls, function)
64 yield TracebackEntry(filename, lineno, function, formatted_args,
65 code_context, index)
66
67def format_extracted(extracted, formatter=None, limit=None):
68 if limit:
69 extracted = extracted[-limit:]
70
71 formatted = []
72 for tracebackinfo in extracted:
73 formatted.extend(tracebackinfo.format(formatter))
74 return formatted
75
76
77def format_exception(etype, value, tb, context=1, limit=None, formatter=None):
78 formatted = ['Traceback (most recent call last):\n']
79
80 if hasattr(tb, 'tb_next'):
81 tb = extract_traceback(tb, context)
82
83 formatted.extend(format_extracted(tb, formatter, limit))
84 formatted.extend(traceback.format_exception_only(etype, value))
85 return formatted
86
87def to_string(exc):
88 if isinstance(exc, SystemExit):
89 if not isinstance(exc.code, basestring):
90 return 'Exited with "%d"' % exc.code
91 return str(exc)
diff --git a/bitbake/lib/bb/fetch2/__init__.py b/bitbake/lib/bb/fetch2/__init__.py
new file mode 100644
index 0000000000..451d104f67
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/__init__.py
@@ -0,0 +1,1538 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementations
5
6Classes for obtaining upstream sources for the
7BitBake build tools.
8"""
9
10# Copyright (C) 2003, 2004 Chris Larson
11# Copyright (C) 2012 Intel Corporation
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26# Based on functions from the base bb module, Copyright 2003 Holger Schurig
27
28from __future__ import absolute_import
29from __future__ import print_function
30import os, re
31import signal
32import glob
33import logging
34import urllib
35import urlparse
36if 'git' not in urlparse.uses_netloc:
37 urlparse.uses_netloc.append('git')
38from urlparse import urlparse
39import operator
40import bb.persist_data, bb.utils
41import bb.checksum
42from bb import data
43import bb.process
44import subprocess
45
46__version__ = "2"
47_checksum_cache = bb.checksum.FileChecksumCache()
48
49logger = logging.getLogger("BitBake.Fetcher")
50
51class BBFetchException(Exception):
52 """Class all fetch exceptions inherit from"""
53 def __init__(self, message):
54 self.msg = message
55 Exception.__init__(self, message)
56
57 def __str__(self):
58 return self.msg
59
60class MalformedUrl(BBFetchException):
61 """Exception raised when encountering an invalid url"""
62 def __init__(self, url):
63 msg = "The URL: '%s' is invalid and cannot be interpreted" % url
64 self.url = url
65 BBFetchException.__init__(self, msg)
66 self.args = (url,)
67
68class FetchError(BBFetchException):
69 """General fetcher exception when something happens incorrectly"""
70 def __init__(self, message, url = None):
71 if url:
72 msg = "Fetcher failure for URL: '%s'. %s" % (url, message)
73 else:
74 msg = "Fetcher failure: %s" % message
75 self.url = url
76 BBFetchException.__init__(self, msg)
77 self.args = (message, url)
78
79class ChecksumError(FetchError):
80 """Exception when mismatched checksum encountered"""
81 def __init__(self, message, url = None, checksum = None):
82 self.checksum = checksum
83 FetchError.__init__(self, message, url)
84
85class NoChecksumError(FetchError):
86 """Exception when no checksum is specified, but BB_STRICT_CHECKSUM is set"""
87
88class UnpackError(BBFetchException):
89 """General fetcher exception when something happens incorrectly when unpacking"""
90 def __init__(self, message, url):
91 msg = "Unpack failure for URL: '%s'. %s" % (url, message)
92 self.url = url
93 BBFetchException.__init__(self, msg)
94 self.args = (message, url)
95
96class NoMethodError(BBFetchException):
97 """Exception raised when there is no method to obtain a supplied url or set of urls"""
98 def __init__(self, url):
99 msg = "Could not find a fetcher which supports the URL: '%s'" % url
100 self.url = url
101 BBFetchException.__init__(self, msg)
102 self.args = (url,)
103
104class MissingParameterError(BBFetchException):
105 """Exception raised when a fetch method is missing a critical parameter in the url"""
106 def __init__(self, missing, url):
107 msg = "URL: '%s' is missing the required parameter '%s'" % (url, missing)
108 self.url = url
109 self.missing = missing
110 BBFetchException.__init__(self, msg)
111 self.args = (missing, url)
112
113class ParameterError(BBFetchException):
114 """Exception raised when a url cannot be proccessed due to invalid parameters."""
115 def __init__(self, message, url):
116 msg = "URL: '%s' has invalid parameters. %s" % (url, message)
117 self.url = url
118 BBFetchException.__init__(self, msg)
119 self.args = (message, url)
120
121class NetworkAccess(BBFetchException):
122 """Exception raised when network access is disabled but it is required."""
123 def __init__(self, url, cmd):
124 msg = "Network access disabled through BB_NO_NETWORK (or set indirectly due to use of BB_FETCH_PREMIRRORONLY) but access requested with command %s (for url %s)" % (cmd, url)
125 self.url = url
126 self.cmd = cmd
127 BBFetchException.__init__(self, msg)
128 self.args = (url, cmd)
129
130class NonLocalMethod(Exception):
131 def __init__(self):
132 Exception.__init__(self)
133
134
135class URI(object):
136 """
137 A class representing a generic URI, with methods for
138 accessing the URI components, and stringifies to the
139 URI.
140
141 It is constructed by calling it with a URI, or setting
142 the attributes manually:
143
144 uri = URI("http://example.com/")
145
146 uri = URI()
147 uri.scheme = 'http'
148 uri.hostname = 'example.com'
149 uri.path = '/'
150
151 It has the following attributes:
152
153 * scheme (read/write)
154 * userinfo (authentication information) (read/write)
155 * username (read/write)
156 * password (read/write)
157
158 Note, password is deprecated as of RFC 3986.
159
160 * hostname (read/write)
161 * port (read/write)
162 * hostport (read only)
163 "hostname:port", if both are set, otherwise just "hostname"
164 * path (read/write)
165 * path_quoted (read/write)
166 A URI quoted version of path
167 * params (dict) (read/write)
168 * relative (bool) (read only)
169 True if this is a "relative URI", (e.g. file:foo.diff)
170
171 It stringifies to the URI itself.
172
173 Some notes about relative URIs: while it's specified that
174 a URI beginning with <scheme>:// should either be directly
175 followed by a hostname or a /, the old URI handling of the
176 fetch2 library did not comform to this. Therefore, this URI
177 class has some kludges to make sure that URIs are parsed in
178 a way comforming to bitbake's current usage. This URI class
179 supports the following:
180
181 file:relative/path.diff (IETF compliant)
182 git:relative/path.git (IETF compliant)
183 git:///absolute/path.git (IETF compliant)
184 file:///absolute/path.diff (IETF compliant)
185
186 file://relative/path.diff (not IETF compliant)
187
188 But it does not support the following:
189
190 file://hostname/absolute/path.diff (would be IETF compliant)
191
192 Note that the last case only applies to a list of
193 "whitelisted" schemes (currently only file://), that requires
194 its URIs to not have a network location.
195 """
196
197 _relative_schemes = ['file', 'git']
198 _netloc_forbidden = ['file']
199
200 def __init__(self, uri=None):
201 self.scheme = ''
202 self.userinfo = ''
203 self.hostname = ''
204 self.port = None
205 self._path = ''
206 self.params = {}
207 self.relative = False
208
209 if not uri:
210 return
211
212 urlp = urlparse(uri)
213 self.scheme = urlp.scheme
214
215 # Convert URI to be relative
216 if urlp.scheme in self._netloc_forbidden:
217 uri = re.sub("(?<=:)//(?!/)", "", uri, 1)
218 urlp = urlparse(uri)
219
220 # Identify if the URI is relative or not
221 if urlp.scheme in self._relative_schemes and \
222 re.compile("^\w+:(?!//)").match(uri):
223 self.relative = True
224
225 if not self.relative:
226 self.hostname = urlp.hostname or ''
227 self.port = urlp.port
228
229 self.userinfo += urlp.username or ''
230
231 if urlp.password:
232 self.userinfo += ':%s' % urlp.password
233
234 # Do support params even for URI schemes that Python's
235 # urlparse doesn't support params for.
236 path = ''
237 param_str = ''
238 if not urlp.params:
239 path, param_str = (list(urlp.path.split(";", 1)) + [None])[:2]
240 else:
241 path = urlp.path
242 param_str = urlp.params
243
244 self.path = urllib.unquote(path)
245
246 if param_str:
247 self.params = self._param_dict(param_str)
248
249 def __str__(self):
250 userinfo = self.userinfo
251 if userinfo:
252 userinfo += '@'
253
254 return "%s:%s%s%s%s%s" % (
255 self.scheme,
256 '' if self.relative else '//',
257 userinfo,
258 self.hostport,
259 self.path_quoted,
260 self._param_str)
261
262 @property
263 def _param_str(self):
264 ret = ''
265 for key, val in self.params.items():
266 ret += ";%s=%s" % (key, val)
267 return ret
268
269 def _param_dict(self, param_str):
270 parm = {}
271
272 for keyval in param_str.split(";"):
273 key, val = keyval.split("=", 1)
274 parm[key] = val
275
276 return parm
277
278 @property
279 def hostport(self):
280 if not self.port:
281 return self.hostname
282 return "%s:%d" % (self.hostname, self.port)
283
284 @property
285 def path_quoted(self):
286 return urllib.quote(self.path)
287
288 @path_quoted.setter
289 def path_quoted(self, path):
290 self.path = urllib.unquote(path)
291
292 @property
293 def path(self):
294 return self._path
295
296 @path.setter
297 def path(self, path):
298 self._path = path
299
300 if re.compile("^/").match(path):
301 self.relative = False
302 else:
303 self.relative = True
304
305 @property
306 def username(self):
307 if self.userinfo:
308 return (self.userinfo.split(":", 1))[0]
309 return ''
310
311 @username.setter
312 def username(self, username):
313 self.userinfo = username
314 if self.password:
315 self.userinfo += ":%s" % self.password
316
317 @property
318 def password(self):
319 if self.userinfo and ":" in self.userinfo:
320 return (self.userinfo.split(":", 1))[1]
321 return ''
322
323 @password.setter
324 def password(self, password):
325 self.userinfo = "%s:%s" % (self.username, password)
326
327def decodeurl(url):
328 """Decodes an URL into the tokens (scheme, network location, path,
329 user, password, parameters).
330 """
331
332 m = re.compile('(?P<type>[^:]*)://((?P<user>.+)@)?(?P<location>[^;]+)(;(?P<parm>.*))?').match(url)
333 if not m:
334 raise MalformedUrl(url)
335
336 type = m.group('type')
337 location = m.group('location')
338 if not location:
339 raise MalformedUrl(url)
340 user = m.group('user')
341 parm = m.group('parm')
342
343 locidx = location.find('/')
344 if locidx != -1 and type.lower() != 'file':
345 host = location[:locidx]
346 path = location[locidx:]
347 else:
348 host = ""
349 path = location
350 if user:
351 m = re.compile('(?P<user>[^:]+)(:?(?P<pswd>.*))').match(user)
352 if m:
353 user = m.group('user')
354 pswd = m.group('pswd')
355 else:
356 user = ''
357 pswd = ''
358
359 p = {}
360 if parm:
361 for s in parm.split(';'):
362 s1, s2 = s.split('=')
363 p[s1] = s2
364
365 return type, host, urllib.unquote(path), user, pswd, p
366
367def encodeurl(decoded):
368 """Encodes a URL from tokens (scheme, network location, path,
369 user, password, parameters).
370 """
371
372 type, host, path, user, pswd, p = decoded
373
374 if not path:
375 raise MissingParameterError('path', "encoded from the data %s" % str(decoded))
376 if not type:
377 raise MissingParameterError('type', "encoded from the data %s" % str(decoded))
378 url = '%s://' % type
379 if user and type != "file":
380 url += "%s" % user
381 if pswd:
382 url += ":%s" % pswd
383 url += "@"
384 if host and type != "file":
385 url += "%s" % host
386 # Standardise path to ensure comparisons work
387 while '//' in path:
388 path = path.replace("//", "/")
389 url += "%s" % urllib.quote(path)
390 if p:
391 for parm in p:
392 url += ";%s=%s" % (parm, p[parm])
393
394 return url
395
396def uri_replace(ud, uri_find, uri_replace, replacements, d):
397 if not ud.url or not uri_find or not uri_replace:
398 logger.error("uri_replace: passed an undefined value, not replacing")
399 return None
400 uri_decoded = list(decodeurl(ud.url))
401 uri_find_decoded = list(decodeurl(uri_find))
402 uri_replace_decoded = list(decodeurl(uri_replace))
403 logger.debug(2, "For url %s comparing %s to %s" % (uri_decoded, uri_find_decoded, uri_replace_decoded))
404 result_decoded = ['', '', '', '', '', {}]
405 for loc, i in enumerate(uri_find_decoded):
406 result_decoded[loc] = uri_decoded[loc]
407 regexp = i
408 if loc == 0 and regexp and not regexp.endswith("$"):
409 # Leaving the type unanchored can mean "https" matching "file" can become "files"
410 # which is clearly undesirable.
411 regexp += "$"
412 if loc == 5:
413 # Handle URL parameters
414 if i:
415 # Any specified URL parameters must match
416 for k in uri_replace_decoded[loc]:
417 if uri_decoded[loc][k] != uri_replace_decoded[loc][k]:
418 return None
419 # Overwrite any specified replacement parameters
420 for k in uri_replace_decoded[loc]:
421 for l in replacements:
422 uri_replace_decoded[loc][k] = uri_replace_decoded[loc][k].replace(l, replacements[l])
423 result_decoded[loc][k] = uri_replace_decoded[loc][k]
424 elif (re.match(regexp, uri_decoded[loc])):
425 if not uri_replace_decoded[loc]:
426 result_decoded[loc] = ""
427 else:
428 for k in replacements:
429 uri_replace_decoded[loc] = uri_replace_decoded[loc].replace(k, replacements[k])
430 #bb.note("%s %s %s" % (regexp, uri_replace_decoded[loc], uri_decoded[loc]))
431 result_decoded[loc] = re.sub(regexp, uri_replace_decoded[loc], uri_decoded[loc])
432 if loc == 2:
433 # Handle path manipulations
434 basename = None
435 if uri_decoded[0] != uri_replace_decoded[0] and ud.mirrortarball:
436 # If the source and destination url types differ, must be a mirrortarball mapping
437 basename = os.path.basename(ud.mirrortarball)
438 # Kill parameters, they make no sense for mirror tarballs
439 uri_decoded[5] = {}
440 elif ud.localpath and ud.method.supports_checksum(ud):
441 basename = os.path.basename(ud.localpath)
442 if basename and not result_decoded[loc].endswith(basename):
443 result_decoded[loc] = os.path.join(result_decoded[loc], basename)
444 else:
445 return None
446 result = encodeurl(result_decoded)
447 if result == ud.url:
448 return None
449 logger.debug(2, "For url %s returning %s" % (ud.url, result))
450 return result
451
452methods = []
453urldata_cache = {}
454saved_headrevs = {}
455
456def fetcher_init(d):
457 """
458 Called to initialize the fetchers once the configuration data is known.
459 Calls before this must not hit the cache.
460 """
461 # When to drop SCM head revisions controlled by user policy
462 srcrev_policy = d.getVar('BB_SRCREV_POLICY', True) or "clear"
463 if srcrev_policy == "cache":
464 logger.debug(1, "Keeping SRCREV cache due to cache policy of: %s", srcrev_policy)
465 elif srcrev_policy == "clear":
466 logger.debug(1, "Clearing SRCREV cache due to cache policy of: %s", srcrev_policy)
467 revs = bb.persist_data.persist('BB_URI_HEADREVS', d)
468 try:
469 bb.fetch2.saved_headrevs = revs.items()
470 except:
471 pass
472 revs.clear()
473 else:
474 raise FetchError("Invalid SRCREV cache policy of: %s" % srcrev_policy)
475
476 _checksum_cache.init_cache(d)
477
478 for m in methods:
479 if hasattr(m, "init"):
480 m.init(d)
481
482def fetcher_parse_save(d):
483 _checksum_cache.save_extras(d)
484
485def fetcher_parse_done(d):
486 _checksum_cache.save_merge(d)
487
488def fetcher_compare_revisions(d):
489 """
490 Compare the revisions in the persistant cache with current values and
491 return true/false on whether they've changed.
492 """
493
494 data = bb.persist_data.persist('BB_URI_HEADREVS', d).items()
495 data2 = bb.fetch2.saved_headrevs
496
497 changed = False
498 for key in data:
499 if key not in data2 or data2[key] != data[key]:
500 logger.debug(1, "%s changed", key)
501 changed = True
502 return True
503 else:
504 logger.debug(2, "%s did not change", key)
505 return False
506
507def mirror_from_string(data):
508 return [ i.split() for i in (data or "").replace('\\n','\n').split('\n') if i ]
509
510def verify_checksum(u, ud, d):
511 """
512 verify the MD5 and SHA256 checksum for downloaded src
513
514 Raises a FetchError if one or both of the SRC_URI checksums do not match
515 the downloaded file, or if BB_STRICT_CHECKSUM is set and there are no
516 checksums specified.
517
518 """
519
520 if not ud.method.supports_checksum(ud):
521 return
522
523 md5data = bb.utils.md5_file(ud.localpath)
524 sha256data = bb.utils.sha256_file(ud.localpath)
525
526 if ud.method.recommends_checksum(ud):
527 # If strict checking enabled and neither sum defined, raise error
528 strict = d.getVar("BB_STRICT_CHECKSUM", True) or None
529 if (strict and ud.md5_expected == None and ud.sha256_expected == None):
530 raise NoChecksumError('No checksum specified for %s, please add at least one to the recipe:\n'
531 'SRC_URI[%s] = "%s"\nSRC_URI[%s] = "%s"' %
532 (ud.localpath, ud.md5_name, md5data,
533 ud.sha256_name, sha256data), u)
534
535 # Log missing sums so user can more easily add them
536 if ud.md5_expected == None:
537 logger.warn('Missing md5 SRC_URI checksum for %s, consider adding to the recipe:\n'
538 'SRC_URI[%s] = "%s"',
539 ud.localpath, ud.md5_name, md5data)
540
541 if ud.sha256_expected == None:
542 logger.warn('Missing sha256 SRC_URI checksum for %s, consider adding to the recipe:\n'
543 'SRC_URI[%s] = "%s"',
544 ud.localpath, ud.sha256_name, sha256data)
545
546 md5mismatch = False
547 sha256mismatch = False
548
549 if ud.md5_expected != md5data:
550 md5mismatch = True
551
552 if ud.sha256_expected != sha256data:
553 sha256mismatch = True
554
555 # We want to alert the user if a checksum is defined in the recipe but
556 # it does not match.
557 msg = ""
558 mismatch = False
559 if md5mismatch and ud.md5_expected:
560 msg = msg + "\nFile: '%s' has %s checksum %s when %s was expected" % (ud.localpath, 'md5', md5data, ud.md5_expected)
561 mismatch = True;
562
563 if sha256mismatch and ud.sha256_expected:
564 msg = msg + "\nFile: '%s' has %s checksum %s when %s was expected" % (ud.localpath, 'sha256', sha256data, ud.sha256_expected)
565 mismatch = True;
566
567 if mismatch:
568 msg = msg + '\nIf this change is expected (e.g. you have upgraded to a new version without updating the checksums) then you can use these lines within the recipe:\nSRC_URI[%s] = "%s"\nSRC_URI[%s] = "%s"\nOtherwise you should retry the download and/or check with upstream to determine if the file has become corrupted or otherwise unexpectedly modified.\n' % (ud.md5_name, md5data, ud.sha256_name, sha256data)
569
570 if len(msg):
571 raise ChecksumError('Checksum mismatch!%s' % msg, u, md5data)
572
573
574def update_stamp(u, ud, d):
575 """
576 donestamp is file stamp indicating the whole fetching is done
577 this function update the stamp after verifying the checksum
578 """
579 if os.path.exists(ud.donestamp):
580 # Touch the done stamp file to show active use of the download
581 try:
582 os.utime(ud.donestamp, None)
583 except:
584 # Errors aren't fatal here
585 pass
586 else:
587 verify_checksum(u, ud, d)
588 open(ud.donestamp, 'w').close()
589
590def subprocess_setup():
591 # Python installs a SIGPIPE handler by default. This is usually not what
592 # non-Python subprocesses expect.
593 # SIGPIPE errors are known issues with gzip/bash
594 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
595
596def get_autorev(d):
597 # only not cache src rev in autorev case
598 if d.getVar('BB_SRCREV_POLICY', True) != "cache":
599 d.setVar('__BB_DONT_CACHE', '1')
600 return "AUTOINC"
601
602def get_srcrev(d):
603 """
604 Return the version string for the current package
605 (usually to be used as PV)
606 Most packages usually only have one SCM so we just pass on the call.
607 In the multi SCM case, we build a value based on SRCREV_FORMAT which must
608 have been set.
609 """
610
611 scms = []
612 fetcher = Fetch(d.getVar('SRC_URI', True).split(), d)
613 urldata = fetcher.ud
614 for u in urldata:
615 if urldata[u].method.supports_srcrev():
616 scms.append(u)
617
618 if len(scms) == 0:
619 raise FetchError("SRCREV was used yet no valid SCM was found in SRC_URI")
620
621 if len(scms) == 1 and len(urldata[scms[0]].names) == 1:
622 autoinc, rev = urldata[scms[0]].method.sortable_revision(scms[0], urldata[scms[0]], d, urldata[scms[0]].names[0])
623 if len(rev) > 10:
624 rev = rev[:10]
625 if autoinc:
626 return "AUTOINC+" + rev
627 return rev
628
629 #
630 # Mutiple SCMs are in SRC_URI so we resort to SRCREV_FORMAT
631 #
632 format = d.getVar('SRCREV_FORMAT', True)
633 if not format:
634 raise FetchError("The SRCREV_FORMAT variable must be set when multiple SCMs are used.")
635
636 seenautoinc = False
637 for scm in scms:
638 ud = urldata[scm]
639 for name in ud.names:
640 autoinc, rev = ud.method.sortable_revision(scm, ud, d, name)
641 seenautoinc = seenautoinc or autoinc
642 if len(rev) > 10:
643 rev = rev[:10]
644 format = format.replace(name, rev)
645 if seenautoinc:
646 format = "AUTOINC+" + format
647
648 return format
649
650def localpath(url, d):
651 fetcher = bb.fetch2.Fetch([url], d)
652 return fetcher.localpath(url)
653
654def runfetchcmd(cmd, d, quiet = False, cleanup = []):
655 """
656 Run cmd returning the command output
657 Raise an error if interrupted or cmd fails
658 Optionally echo command output to stdout
659 Optionally remove the files/directories listed in cleanup upon failure
660 """
661
662 # Need to export PATH as binary could be in metadata paths
663 # rather than host provided
664 # Also include some other variables.
665 # FIXME: Should really include all export varaiables?
666 exportvars = ['HOME', 'PATH',
667 'HTTP_PROXY', 'http_proxy',
668 'HTTPS_PROXY', 'https_proxy',
669 'FTP_PROXY', 'ftp_proxy',
670 'FTPS_PROXY', 'ftps_proxy',
671 'NO_PROXY', 'no_proxy',
672 'ALL_PROXY', 'all_proxy',
673 'GIT_PROXY_COMMAND',
674 'SSH_AUTH_SOCK', 'SSH_AGENT_PID',
675 'SOCKS5_USER', 'SOCKS5_PASSWD']
676
677 for var in exportvars:
678 val = d.getVar(var, True)
679 if val:
680 cmd = 'export ' + var + '=\"%s\"; %s' % (val, cmd)
681
682 logger.debug(1, "Running %s", cmd)
683
684 success = False
685 error_message = ""
686
687 try:
688 (output, errors) = bb.process.run(cmd, shell=True, stderr=subprocess.PIPE)
689 success = True
690 except bb.process.NotFoundError as e:
691 error_message = "Fetch command %s" % (e.command)
692 except bb.process.ExecutionError as e:
693 if e.stdout:
694 output = "output:\n%s\n%s" % (e.stdout, e.stderr)
695 elif e.stderr:
696 output = "output:\n%s" % e.stderr
697 else:
698 output = "no output"
699 error_message = "Fetch command failed with exit code %s, %s" % (e.exitcode, output)
700 except bb.process.CmdError as e:
701 error_message = "Fetch command %s could not be run:\n%s" % (e.command, e.msg)
702 if not success:
703 for f in cleanup:
704 try:
705 bb.utils.remove(f, True)
706 except OSError:
707 pass
708
709 raise FetchError(error_message)
710
711 return output
712
713def check_network_access(d, info = "", url = None):
714 """
715 log remote network access, and error if BB_NO_NETWORK is set
716 """
717 if d.getVar("BB_NO_NETWORK", True) == "1":
718 raise NetworkAccess(url, info)
719 else:
720 logger.debug(1, "Fetcher accessed the network with the command %s" % info)
721
722def build_mirroruris(origud, mirrors, ld):
723 uris = []
724 uds = []
725
726 replacements = {}
727 replacements["TYPE"] = origud.type
728 replacements["HOST"] = origud.host
729 replacements["PATH"] = origud.path
730 replacements["BASENAME"] = origud.path.split("/")[-1]
731 replacements["MIRRORNAME"] = origud.host.replace(':','.') + origud.path.replace('/', '.').replace('*', '.')
732
733 def adduri(uri, ud, uris, uds):
734 for line in mirrors:
735 try:
736 (find, replace) = line
737 except ValueError:
738 continue
739 newuri = uri_replace(ud, find, replace, replacements, ld)
740 if not newuri or newuri in uris or newuri == origud.url:
741 continue
742 try:
743 newud = FetchData(newuri, ld)
744 newud.setup_localpath(ld)
745 except bb.fetch2.BBFetchException as e:
746 logger.debug(1, "Mirror fetch failure for url %s (original url: %s)" % (newuri, origud.url))
747 logger.debug(1, str(e))
748 try:
749 ud.method.clean(ud, ld)
750 except UnboundLocalError:
751 pass
752 continue
753 uris.append(newuri)
754 uds.append(newud)
755
756 adduri(newuri, newud, uris, uds)
757
758 adduri(None, origud, uris, uds)
759
760 return uris, uds
761
762def rename_bad_checksum(ud, suffix):
763 """
764 Renames files to have suffix from parameter
765 """
766
767 if ud.localpath is None:
768 return
769
770 new_localpath = "%s_bad-checksum_%s" % (ud.localpath, suffix)
771 bb.warn("Renaming %s to %s" % (ud.localpath, new_localpath))
772 bb.utils.movefile(ud.localpath, new_localpath)
773
774
775def try_mirror_url(newuri, origud, ud, ld, check = False):
776 # Return of None or a value means we're finished
777 # False means try another url
778 try:
779 if check:
780 found = ud.method.checkstatus(newuri, ud, ld)
781 if found:
782 return found
783 return False
784
785 os.chdir(ld.getVar("DL_DIR", True))
786
787 if not os.path.exists(ud.donestamp) or ud.method.need_update(newuri, ud, ld):
788 ud.method.download(newuri, ud, ld)
789 if hasattr(ud.method,"build_mirror_data"):
790 ud.method.build_mirror_data(newuri, ud, ld)
791
792 if not ud.localpath or not os.path.exists(ud.localpath):
793 return False
794
795 if ud.localpath == origud.localpath:
796 return ud.localpath
797
798 # We may be obtaining a mirror tarball which needs further processing by the real fetcher
799 # If that tarball is a local file:// we need to provide a symlink to it
800 dldir = ld.getVar("DL_DIR", True)
801 if origud.mirrortarball and os.path.basename(ud.localpath) == os.path.basename(origud.mirrortarball) \
802 and os.path.basename(ud.localpath) != os.path.basename(origud.localpath):
803 bb.utils.mkdirhier(os.path.dirname(ud.donestamp))
804 open(ud.donestamp, 'w').close()
805 dest = os.path.join(dldir, os.path.basename(ud.localpath))
806 if not os.path.exists(dest):
807 os.symlink(ud.localpath, dest)
808 return None
809 # Otherwise the result is a local file:// and we symlink to it
810 if not os.path.exists(origud.localpath):
811 if os.path.islink(origud.localpath):
812 # Broken symbolic link
813 os.unlink(origud.localpath)
814
815 os.symlink(ud.localpath, origud.localpath)
816 update_stamp(newuri, origud, ld)
817 return ud.localpath
818
819 except bb.fetch2.NetworkAccess:
820 raise
821
822 except bb.fetch2.BBFetchException as e:
823 if isinstance(e, ChecksumError):
824 logger.warn("Mirror checksum failure for url %s (original url: %s)\nCleaning and trying again." % (newuri, origud.url))
825 logger.warn(str(e))
826 rename_bad_checksum(ud, e.checksum)
827 elif isinstance(e, NoChecksumError):
828 raise
829 else:
830 logger.debug(1, "Mirror fetch failure for url %s (original url: %s)" % (newuri, origud.url))
831 logger.debug(1, str(e))
832 try:
833 ud.method.clean(ud, ld)
834 except UnboundLocalError:
835 pass
836 return False
837
838def try_mirrors(d, origud, mirrors, check = False):
839 """
840 Try to use a mirrored version of the sources.
841 This method will be automatically called before the fetchers go.
842
843 d Is a bb.data instance
844 uri is the original uri we're trying to download
845 mirrors is the list of mirrors we're going to try
846 """
847 ld = d.createCopy()
848
849 uris, uds = build_mirroruris(origud, mirrors, ld)
850
851 for index, uri in enumerate(uris):
852 ret = try_mirror_url(uri, origud, uds[index], ld, check)
853 if ret != False:
854 return ret
855 return None
856
857def srcrev_internal_helper(ud, d, name):
858 """
859 Return:
860 a) a source revision if specified
861 b) latest revision if SRCREV="AUTOINC"
862 c) None if not specified
863 """
864
865 if 'rev' in ud.parm:
866 return ud.parm['rev']
867
868 if 'tag' in ud.parm:
869 return ud.parm['tag']
870
871 rev = None
872 pn = d.getVar("PN", True)
873 if name != '':
874 rev = d.getVar("SRCREV_%s_pn-%s" % (name, pn), True)
875 if not rev:
876 rev = d.getVar("SRCREV_%s" % name, True)
877 if not rev:
878 rev = d.getVar("SRCREV_pn-%s" % pn, True)
879 if not rev:
880 rev = d.getVar("SRCREV", True)
881 if rev == "INVALID":
882 var = "SRCREV_pn-%s" % pn
883 if name != '':
884 var = "SRCREV_%s_pn-%s" % (name, pn)
885 raise FetchError("Please set %s to a valid value" % var, ud.url)
886 if rev == "AUTOINC":
887 rev = ud.method.latest_revision(ud.url, ud, d, name)
888
889 return rev
890
891
892def get_checksum_file_list(d):
893 """ Get a list of files checksum in SRC_URI
894
895 Returns the resolved local paths of all local file entries in
896 SRC_URI as a space-separated string
897 """
898 fetch = Fetch([], d, cache = False, localonly = True)
899
900 dl_dir = d.getVar('DL_DIR', True)
901 filelist = []
902 for u in fetch.urls:
903 ud = fetch.ud[u]
904
905 if ud and isinstance(ud.method, local.Local):
906 ud.setup_localpath(d)
907 f = ud.localpath
908 if f.startswith(dl_dir):
909 # The local fetcher's behaviour is to return a path under DL_DIR if it couldn't find the file anywhere else
910 if os.path.exists(f):
911 bb.warn("Getting checksum for %s SRC_URI entry %s: file not found except in DL_DIR" % (d.getVar('PN', True), os.path.basename(f)))
912 else:
913 bb.warn("Unable to get checksum for %s SRC_URI entry %s: file could not be found" % (d.getVar('PN', True), os.path.basename(f)))
914 continue
915 filelist.append(f)
916
917 return " ".join(filelist)
918
919
920def get_file_checksums(filelist, pn):
921 """Get a list of the checksums for a list of local files
922
923 Returns the checksums for a list of local files, caching the results as
924 it proceeds
925
926 """
927
928 def checksum_file(f):
929 try:
930 checksum = _checksum_cache.get_checksum(f)
931 except OSError as e:
932 bb.warn("Unable to get checksum for %s SRC_URI entry %s: %s" % (pn, os.path.basename(f), e))
933 return None
934 return checksum
935
936 checksums = []
937 for pth in filelist.split():
938 checksum = None
939 if '*' in pth:
940 # Handle globs
941 for f in glob.glob(pth):
942 checksum = checksum_file(f)
943 if checksum:
944 checksums.append((f, checksum))
945 elif os.path.isdir(pth):
946 # Handle directories
947 for root, dirs, files in os.walk(pth):
948 for name in files:
949 fullpth = os.path.join(root, name)
950 checksum = checksum_file(fullpth)
951 if checksum:
952 checksums.append((fullpth, checksum))
953 else:
954 checksum = checksum_file(pth)
955
956 if checksum:
957 checksums.append((pth, checksum))
958
959 checksums.sort(key=operator.itemgetter(1))
960 return checksums
961
962
963class FetchData(object):
964 """
965 A class which represents the fetcher state for a given URI.
966 """
967 def __init__(self, url, d, localonly = False):
968 # localpath is the location of a downloaded result. If not set, the file is local.
969 self.donestamp = None
970 self.localfile = ""
971 self.localpath = None
972 self.lockfile = None
973 self.mirrortarball = None
974 self.basename = None
975 self.basepath = None
976 (self.type, self.host, self.path, self.user, self.pswd, self.parm) = decodeurl(data.expand(url, d))
977 self.date = self.getSRCDate(d)
978 self.url = url
979 if not self.user and "user" in self.parm:
980 self.user = self.parm["user"]
981 if not self.pswd and "pswd" in self.parm:
982 self.pswd = self.parm["pswd"]
983 self.setup = False
984
985 if "name" in self.parm:
986 self.md5_name = "%s.md5sum" % self.parm["name"]
987 self.sha256_name = "%s.sha256sum" % self.parm["name"]
988 else:
989 self.md5_name = "md5sum"
990 self.sha256_name = "sha256sum"
991 if self.md5_name in self.parm:
992 self.md5_expected = self.parm[self.md5_name]
993 elif self.type not in ["http", "https", "ftp", "ftps", "sftp"]:
994 self.md5_expected = None
995 else:
996 self.md5_expected = d.getVarFlag("SRC_URI", self.md5_name)
997 if self.sha256_name in self.parm:
998 self.sha256_expected = self.parm[self.sha256_name]
999 elif self.type not in ["http", "https", "ftp", "ftps", "sftp"]:
1000 self.sha256_expected = None
1001 else:
1002 self.sha256_expected = d.getVarFlag("SRC_URI", self.sha256_name)
1003
1004 self.names = self.parm.get("name",'default').split(',')
1005
1006 self.method = None
1007 for m in methods:
1008 if m.supports(url, self, d):
1009 self.method = m
1010 break
1011
1012 if not self.method:
1013 raise NoMethodError(url)
1014
1015 if localonly and not isinstance(self.method, local.Local):
1016 raise NonLocalMethod()
1017
1018 if self.parm.get("proto", None) and "protocol" not in self.parm:
1019 logger.warn('Consider updating %s recipe to use "protocol" not "proto" in SRC_URI.', d.getVar('PN', True))
1020 self.parm["protocol"] = self.parm.get("proto", None)
1021
1022 if hasattr(self.method, "urldata_init"):
1023 self.method.urldata_init(self, d)
1024
1025 if "localpath" in self.parm:
1026 # if user sets localpath for file, use it instead.
1027 self.localpath = self.parm["localpath"]
1028 self.basename = os.path.basename(self.localpath)
1029 elif self.localfile:
1030 self.localpath = self.method.localpath(self.url, self, d)
1031
1032 dldir = d.getVar("DL_DIR", True)
1033 # Note: .done and .lock files should always be in DL_DIR whereas localpath may not be.
1034 if self.localpath and self.localpath.startswith(dldir):
1035 basepath = self.localpath
1036 elif self.localpath:
1037 basepath = dldir + os.sep + os.path.basename(self.localpath)
1038 else:
1039 basepath = dldir + os.sep + (self.basepath or self.basename)
1040 self.donestamp = basepath + '.done'
1041 self.lockfile = basepath + '.lock'
1042
1043 def setup_revisons(self, d):
1044 self.revisions = {}
1045 for name in self.names:
1046 self.revisions[name] = srcrev_internal_helper(self, d, name)
1047
1048 # add compatibility code for non name specified case
1049 if len(self.names) == 1:
1050 self.revision = self.revisions[self.names[0]]
1051
1052 def setup_localpath(self, d):
1053 if not self.localpath:
1054 self.localpath = self.method.localpath(self.url, self, d)
1055
1056 def getSRCDate(self, d):
1057 """
1058 Return the SRC Date for the component
1059
1060 d the bb.data module
1061 """
1062 if "srcdate" in self.parm:
1063 return self.parm['srcdate']
1064
1065 pn = d.getVar("PN", True)
1066
1067 if pn:
1068 return d.getVar("SRCDATE_%s" % pn, True) or d.getVar("SRCDATE", True) or d.getVar("DATE", True)
1069
1070 return d.getVar("SRCDATE", True) or d.getVar("DATE", True)
1071
1072class FetchMethod(object):
1073 """Base class for 'fetch'ing data"""
1074
1075 def __init__(self, urls = []):
1076 self.urls = []
1077
1078 def supports(self, url, urldata, d):
1079 """
1080 Check to see if this fetch class supports a given url.
1081 """
1082 return 0
1083
1084 def localpath(self, url, urldata, d):
1085 """
1086 Return the local filename of a given url assuming a successful fetch.
1087 Can also setup variables in urldata for use in go (saving code duplication
1088 and duplicate code execution)
1089 """
1090 return os.path.join(data.getVar("DL_DIR", d, True), urldata.localfile)
1091
1092 def supports_checksum(self, urldata):
1093 """
1094 Is localpath something that can be represented by a checksum?
1095 """
1096
1097 # We cannot compute checksums for directories
1098 if os.path.isdir(urldata.localpath) == True:
1099 return False
1100 if urldata.localpath.find("*") != -1:
1101 return False
1102
1103 return True
1104
1105 def recommends_checksum(self, urldata):
1106 """
1107 Is the backend on where checksumming is recommended (should warnings
1108 be displayed if there is no checksum)?
1109 """
1110 return False
1111
1112 def _strip_leading_slashes(self, relpath):
1113 """
1114 Remove leading slash as os.path.join can't cope
1115 """
1116 while os.path.isabs(relpath):
1117 relpath = relpath[1:]
1118 return relpath
1119
1120 def setUrls(self, urls):
1121 self.__urls = urls
1122
1123 def getUrls(self):
1124 return self.__urls
1125
1126 urls = property(getUrls, setUrls, None, "Urls property")
1127
1128 def need_update(self, url, ud, d):
1129 """
1130 Force a fetch, even if localpath exists?
1131 """
1132 if os.path.exists(ud.localpath):
1133 return False
1134 return True
1135
1136 def supports_srcrev(self):
1137 """
1138 The fetcher supports auto source revisions (SRCREV)
1139 """
1140 return False
1141
1142 def download(self, url, urldata, d):
1143 """
1144 Fetch urls
1145 Assumes localpath was called first
1146 """
1147 raise NoMethodError(url)
1148
1149 def unpack(self, urldata, rootdir, data):
1150 iterate = False
1151 file = urldata.localpath
1152
1153 try:
1154 unpack = bb.utils.to_boolean(urldata.parm.get('unpack'), True)
1155 except ValueError as exc:
1156 bb.fatal("Invalid value for 'unpack' parameter for %s: %s" %
1157 (file, urldata.parm.get('unpack')))
1158
1159 dots = file.split(".")
1160 if dots[-1] in ['gz', 'bz2', 'Z', 'xz']:
1161 efile = os.path.join(rootdir, os.path.basename('.'.join(dots[0:-1])))
1162 else:
1163 efile = file
1164 cmd = None
1165
1166 if unpack:
1167 if file.endswith('.tar'):
1168 cmd = 'tar x --no-same-owner -f %s' % file
1169 elif file.endswith('.tgz') or file.endswith('.tar.gz') or file.endswith('.tar.Z'):
1170 cmd = 'tar xz --no-same-owner -f %s' % file
1171 elif file.endswith('.tbz') or file.endswith('.tbz2') or file.endswith('.tar.bz2'):
1172 cmd = 'bzip2 -dc %s | tar x --no-same-owner -f -' % file
1173 elif file.endswith('.gz') or file.endswith('.Z') or file.endswith('.z'):
1174 cmd = 'gzip -dc %s > %s' % (file, efile)
1175 elif file.endswith('.bz2'):
1176 cmd = 'bzip2 -dc %s > %s' % (file, efile)
1177 elif file.endswith('.tar.xz'):
1178 cmd = 'xz -dc %s | tar x --no-same-owner -f -' % file
1179 elif file.endswith('.xz'):
1180 cmd = 'xz -dc %s > %s' % (file, efile)
1181 elif file.endswith('.zip') or file.endswith('.jar'):
1182 try:
1183 dos = bb.utils.to_boolean(urldata.parm.get('dos'), False)
1184 except ValueError as exc:
1185 bb.fatal("Invalid value for 'dos' parameter for %s: %s" %
1186 (file, urldata.parm.get('dos')))
1187 cmd = 'unzip -q -o'
1188 if dos:
1189 cmd = '%s -a' % cmd
1190 cmd = "%s '%s'" % (cmd, file)
1191 elif file.endswith('.rpm') or file.endswith('.srpm'):
1192 if 'extract' in urldata.parm:
1193 unpack_file = urldata.parm.get('extract')
1194 cmd = 'rpm2cpio.sh %s | cpio -id %s' % (file, unpack_file)
1195 iterate = True
1196 iterate_file = unpack_file
1197 else:
1198 cmd = 'rpm2cpio.sh %s | cpio -id' % (file)
1199 elif file.endswith('.deb') or file.endswith('.ipk'):
1200 cmd = 'ar -p %s data.tar.gz | zcat | tar --no-same-owner -xpf -' % file
1201
1202 if not unpack or not cmd:
1203 # If file == dest, then avoid any copies, as we already put the file into dest!
1204 dest = os.path.join(rootdir, os.path.basename(file))
1205 if (file != dest) and not (os.path.exists(dest) and os.path.samefile(file, dest)):
1206 if os.path.isdir(file):
1207 # If for example we're asked to copy file://foo/bar, we need to unpack the result into foo/bar
1208 basepath = getattr(urldata, "basepath", None)
1209 destdir = "."
1210 if basepath and basepath.endswith("/"):
1211 basepath = basepath.rstrip("/")
1212 elif basepath:
1213 basepath = os.path.dirname(basepath)
1214 if basepath and basepath.find("/") != -1:
1215 destdir = basepath[:basepath.rfind('/')]
1216 destdir = destdir.strip('/')
1217 if destdir != "." and not os.access("%s/%s" % (rootdir, destdir), os.F_OK):
1218 os.makedirs("%s/%s" % (rootdir, destdir))
1219 cmd = 'cp -pPR %s %s/%s/' % (file, rootdir, destdir)
1220 #cmd = 'tar -cf - -C "%d" -ps . | tar -xf - -C "%s/%s/"' % (file, rootdir, destdir)
1221 else:
1222 # The "destdir" handling was specifically done for FILESPATH
1223 # items. So, only do so for file:// entries.
1224 if urldata.type == "file" and urldata.path.find("/") != -1:
1225 destdir = urldata.path.rsplit("/", 1)[0]
1226 else:
1227 destdir = "."
1228 bb.utils.mkdirhier("%s/%s" % (rootdir, destdir))
1229 cmd = 'cp %s %s/%s/' % (file, rootdir, destdir)
1230
1231 if not cmd:
1232 return
1233
1234 # Change to subdir before executing command
1235 save_cwd = os.getcwd();
1236 os.chdir(rootdir)
1237 if 'subdir' in urldata.parm:
1238 newdir = ("%s/%s" % (rootdir, urldata.parm.get('subdir')))
1239 bb.utils.mkdirhier(newdir)
1240 os.chdir(newdir)
1241
1242 path = data.getVar('PATH', True)
1243 if path:
1244 cmd = "PATH=\"%s\" %s" % (path, cmd)
1245 bb.note("Unpacking %s to %s/" % (file, os.getcwd()))
1246 ret = subprocess.call(cmd, preexec_fn=subprocess_setup, shell=True)
1247
1248 os.chdir(save_cwd)
1249
1250 if ret != 0:
1251 raise UnpackError("Unpack command %s failed with return value %s" % (cmd, ret), urldata.url)
1252
1253 if iterate is True:
1254 iterate_urldata = urldata
1255 iterate_urldata.localpath = "%s/%s" % (rootdir, iterate_file)
1256 self.unpack(urldata, rootdir, data)
1257
1258 return
1259
1260 def clean(self, urldata, d):
1261 """
1262 Clean any existing full or partial download
1263 """
1264 bb.utils.remove(urldata.localpath)
1265
1266 def try_premirror(self, url, urldata, d):
1267 """
1268 Should premirrors be used?
1269 """
1270 return True
1271
1272 def checkstatus(self, url, urldata, d):
1273 """
1274 Check the status of a URL
1275 Assumes localpath was called first
1276 """
1277 logger.info("URL %s could not be checked for status since no method exists.", url)
1278 return True
1279
1280 def latest_revision(self, url, ud, d, name):
1281 """
1282 Look in the cache for the latest revision, if not present ask the SCM.
1283 """
1284 if not hasattr(self, "_latest_revision"):
1285 raise ParameterError("The fetcher for this URL does not support _latest_revision", url)
1286
1287 revs = bb.persist_data.persist('BB_URI_HEADREVS', d)
1288 key = self.generate_revision_key(url, ud, d, name)
1289 try:
1290 return revs[key]
1291 except KeyError:
1292 revs[key] = rev = self._latest_revision(url, ud, d, name)
1293 return rev
1294
1295 def sortable_revision(self, url, ud, d, name):
1296 latest_rev = self._build_revision(url, ud, d, name)
1297 return True, str(latest_rev)
1298
1299 def generate_revision_key(self, url, ud, d, name):
1300 key = self._revision_key(url, ud, d, name)
1301 return "%s-%s" % (key, d.getVar("PN", True) or "")
1302
1303class Fetch(object):
1304 def __init__(self, urls, d, cache = True, localonly = False):
1305 if localonly and cache:
1306 raise Exception("bb.fetch2.Fetch.__init__: cannot set cache and localonly at same time")
1307
1308 if len(urls) == 0:
1309 urls = d.getVar("SRC_URI", True).split()
1310 self.urls = urls
1311 self.d = d
1312 self.ud = {}
1313
1314 fn = d.getVar('FILE', True)
1315 if cache and fn and fn in urldata_cache:
1316 self.ud = urldata_cache[fn]
1317
1318 for url in urls:
1319 if url not in self.ud:
1320 try:
1321 self.ud[url] = FetchData(url, d, localonly)
1322 except NonLocalMethod:
1323 if localonly:
1324 self.ud[url] = None
1325 pass
1326
1327 if fn and cache:
1328 urldata_cache[fn] = self.ud
1329
1330 def localpath(self, url):
1331 if url not in self.urls:
1332 self.ud[url] = FetchData(url, self.d)
1333
1334 self.ud[url].setup_localpath(self.d)
1335 return self.d.expand(self.ud[url].localpath)
1336
1337 def localpaths(self):
1338 """
1339 Return a list of the local filenames, assuming successful fetch
1340 """
1341 local = []
1342
1343 for u in self.urls:
1344 ud = self.ud[u]
1345 ud.setup_localpath(self.d)
1346 local.append(ud.localpath)
1347
1348 return local
1349
1350 def download(self, urls = []):
1351 """
1352 Fetch all urls
1353 """
1354 if len(urls) == 0:
1355 urls = self.urls
1356
1357 network = self.d.getVar("BB_NO_NETWORK", True)
1358 premirroronly = (self.d.getVar("BB_FETCH_PREMIRRORONLY", True) == "1")
1359
1360 for u in urls:
1361 ud = self.ud[u]
1362 ud.setup_localpath(self.d)
1363 m = ud.method
1364 localpath = ""
1365
1366 lf = bb.utils.lockfile(ud.lockfile)
1367
1368 try:
1369 self.d.setVar("BB_NO_NETWORK", network)
1370
1371 if os.path.exists(ud.donestamp) and not m.need_update(u, ud, self.d):
1372 localpath = ud.localpath
1373 elif m.try_premirror(u, ud, self.d):
1374 logger.debug(1, "Trying PREMIRRORS")
1375 mirrors = mirror_from_string(self.d.getVar('PREMIRRORS', True))
1376 localpath = try_mirrors(self.d, ud, mirrors, False)
1377
1378 if premirroronly:
1379 self.d.setVar("BB_NO_NETWORK", "1")
1380
1381 os.chdir(self.d.getVar("DL_DIR", True))
1382
1383 firsterr = None
1384 if not localpath and ((not os.path.exists(ud.donestamp)) or m.need_update(u, ud, self.d)):
1385 try:
1386 logger.debug(1, "Trying Upstream")
1387 m.download(u, ud, self.d)
1388 if hasattr(m, "build_mirror_data"):
1389 m.build_mirror_data(u, ud, self.d)
1390 localpath = ud.localpath
1391 # early checksum verify, so that if checksum mismatched,
1392 # fetcher still have chance to fetch from mirror
1393 update_stamp(u, ud, self.d)
1394
1395 except bb.fetch2.NetworkAccess:
1396 raise
1397
1398 except BBFetchException as e:
1399 if isinstance(e, ChecksumError):
1400 logger.warn("Checksum failure encountered with download of %s - will attempt other sources if available" % u)
1401 logger.debug(1, str(e))
1402 rename_bad_checksum(ud, e.checksum)
1403 elif isinstance(e, NoChecksumError):
1404 raise
1405 else:
1406 logger.warn('Failed to fetch URL %s, attempting MIRRORS if available' % u)
1407 logger.debug(1, str(e))
1408 firsterr = e
1409 # Remove any incomplete fetch
1410 m.clean(ud, self.d)
1411 logger.debug(1, "Trying MIRRORS")
1412 mirrors = mirror_from_string(self.d.getVar('MIRRORS', True))
1413 localpath = try_mirrors (self.d, ud, mirrors)
1414
1415 if not localpath or ((not os.path.exists(localpath)) and localpath.find("*") == -1):
1416 if firsterr:
1417 logger.error(str(firsterr))
1418 raise FetchError("Unable to fetch URL from any source.", u)
1419
1420 update_stamp(u, ud, self.d)
1421
1422 except BBFetchException as e:
1423 if isinstance(e, NoChecksumError):
1424 logger.error("%s" % str(e))
1425 elif isinstance(e, ChecksumError):
1426 logger.error("Checksum failure fetching %s" % u)
1427 raise
1428
1429 finally:
1430 bb.utils.unlockfile(lf)
1431
1432 def checkstatus(self, urls = []):
1433 """
1434 Check all urls exist upstream
1435 """
1436
1437 if len(urls) == 0:
1438 urls = self.urls
1439
1440 for u in urls:
1441 ud = self.ud[u]
1442 ud.setup_localpath(self.d)
1443 m = ud.method
1444 logger.debug(1, "Testing URL %s", u)
1445 # First try checking uri, u, from PREMIRRORS
1446 mirrors = mirror_from_string(self.d.getVar('PREMIRRORS', True))
1447 ret = try_mirrors(self.d, ud, mirrors, True)
1448 if not ret:
1449 # Next try checking from the original uri, u
1450 try:
1451 ret = m.checkstatus(u, ud, self.d)
1452 except:
1453 # Finally, try checking uri, u, from MIRRORS
1454 mirrors = mirror_from_string(self.d.getVar('MIRRORS', True))
1455 ret = try_mirrors(self.d, ud, mirrors, True)
1456
1457 if not ret:
1458 raise FetchError("URL %s doesn't work" % u, u)
1459
1460 def unpack(self, root, urls = []):
1461 """
1462 Check all urls exist upstream
1463 """
1464
1465 if len(urls) == 0:
1466 urls = self.urls
1467
1468 for u in urls:
1469 ud = self.ud[u]
1470 ud.setup_localpath(self.d)
1471
1472 if self.d.expand(self.localpath) is None:
1473 continue
1474
1475 if ud.lockfile:
1476 lf = bb.utils.lockfile(ud.lockfile)
1477
1478 ud.method.unpack(ud, root, self.d)
1479
1480 if ud.lockfile:
1481 bb.utils.unlockfile(lf)
1482
1483 def clean(self, urls = []):
1484 """
1485 Clean files that the fetcher gets or places
1486 """
1487
1488 if len(urls) == 0:
1489 urls = self.urls
1490
1491 for url in urls:
1492 if url not in self.ud:
1493 self.ud[url] = FetchData(url, d)
1494 ud = self.ud[url]
1495 ud.setup_localpath(self.d)
1496
1497 if not ud.localfile or self.localpath is None:
1498 continue
1499
1500 if ud.lockfile:
1501 lf = bb.utils.lockfile(ud.lockfile)
1502
1503 ud.method.clean(ud, self.d)
1504 if ud.donestamp:
1505 bb.utils.remove(ud.donestamp)
1506
1507 if ud.lockfile:
1508 bb.utils.unlockfile(lf)
1509
1510from . import cvs
1511from . import git
1512from . import gitsm
1513from . import local
1514from . import svn
1515from . import wget
1516from . import svk
1517from . import ssh
1518from . import sftp
1519from . import perforce
1520from . import bzr
1521from . import hg
1522from . import osc
1523from . import repo
1524
1525methods.append(local.Local())
1526methods.append(wget.Wget())
1527methods.append(svn.Svn())
1528methods.append(git.Git())
1529methods.append(gitsm.GitSM())
1530methods.append(cvs.Cvs())
1531methods.append(svk.Svk())
1532methods.append(ssh.SSH())
1533methods.append(sftp.SFTP())
1534methods.append(perforce.Perforce())
1535methods.append(bzr.Bzr())
1536methods.append(hg.Hg())
1537methods.append(osc.Osc())
1538methods.append(repo.Repo())
diff --git a/bitbake/lib/bb/fetch2/bzr.py b/bitbake/lib/bb/fetch2/bzr.py
new file mode 100644
index 0000000000..5d9e5f907c
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/bzr.py
@@ -0,0 +1,143 @@
1"""
2BitBake 'Fetch' implementation for bzr.
3
4"""
5
6# Copyright (C) 2007 Ross Burton
7# Copyright (C) 2007 Richard Purdie
8#
9# Classes for obtaining upstream sources for the
10# BitBake build tools.
11# Copyright (C) 2003, 2004 Chris Larson
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25
26import os
27import sys
28import logging
29import bb
30from bb import data
31from bb.fetch2 import FetchMethod
32from bb.fetch2 import FetchError
33from bb.fetch2 import runfetchcmd
34from bb.fetch2 import logger
35
36class Bzr(FetchMethod):
37 def supports(self, url, ud, d):
38 return ud.type in ['bzr']
39
40 def urldata_init(self, ud, d):
41 """
42 init bzr specific variable within url data
43 """
44 # Create paths to bzr checkouts
45 relpath = self._strip_leading_slashes(ud.path)
46 ud.pkgdir = os.path.join(data.expand('${BZRDIR}', d), ud.host, relpath)
47
48 ud.setup_revisons(d)
49
50 if not ud.revision:
51 ud.revision = self.latest_revision(ud.url, ud, d)
52
53 ud.localfile = data.expand('bzr_%s_%s_%s.tar.gz' % (ud.host, ud.path.replace('/', '.'), ud.revision), d)
54
55 def _buildbzrcommand(self, ud, d, command):
56 """
57 Build up an bzr commandline based on ud
58 command is "fetch", "update", "revno"
59 """
60
61 basecmd = data.expand('${FETCHCMD_bzr}', d)
62
63 proto = ud.parm.get('protocol', 'http')
64
65 bzrroot = ud.host + ud.path
66
67 options = []
68
69 if command == "revno":
70 bzrcmd = "%s revno %s %s://%s" % (basecmd, " ".join(options), proto, bzrroot)
71 else:
72 if ud.revision:
73 options.append("-r %s" % ud.revision)
74
75 if command == "fetch":
76 bzrcmd = "%s branch %s %s://%s" % (basecmd, " ".join(options), proto, bzrroot)
77 elif command == "update":
78 bzrcmd = "%s pull %s --overwrite" % (basecmd, " ".join(options))
79 else:
80 raise FetchError("Invalid bzr command %s" % command, ud.url)
81
82 return bzrcmd
83
84 def download(self, loc, ud, d):
85 """Fetch url"""
86
87 if os.access(os.path.join(ud.pkgdir, os.path.basename(ud.pkgdir), '.bzr'), os.R_OK):
88 bzrcmd = self._buildbzrcommand(ud, d, "update")
89 logger.debug(1, "BZR Update %s", loc)
90 bb.fetch2.check_network_access(d, bzrcmd, ud.url)
91 os.chdir(os.path.join (ud.pkgdir, os.path.basename(ud.path)))
92 runfetchcmd(bzrcmd, d)
93 else:
94 bb.utils.remove(os.path.join(ud.pkgdir, os.path.basename(ud.pkgdir)), True)
95 bzrcmd = self._buildbzrcommand(ud, d, "fetch")
96 bb.fetch2.check_network_access(d, bzrcmd, ud.url)
97 logger.debug(1, "BZR Checkout %s", loc)
98 bb.utils.mkdirhier(ud.pkgdir)
99 os.chdir(ud.pkgdir)
100 logger.debug(1, "Running %s", bzrcmd)
101 runfetchcmd(bzrcmd, d)
102
103 os.chdir(ud.pkgdir)
104
105 scmdata = ud.parm.get("scmdata", "")
106 if scmdata == "keep":
107 tar_flags = ""
108 else:
109 tar_flags = "--exclude '.bzr' --exclude '.bzrtags'"
110
111 # tar them up to a defined filename
112 runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, os.path.basename(ud.pkgdir)), d, cleanup = [ud.localpath])
113
114 def supports_srcrev(self):
115 return True
116
117 def _revision_key(self, url, ud, d, name):
118 """
119 Return a unique key for the url
120 """
121 return "bzr:" + ud.pkgdir
122
123 def _latest_revision(self, url, ud, d, name):
124 """
125 Return the latest upstream revision number
126 """
127 logger.debug(2, "BZR fetcher hitting network for %s", url)
128
129 bb.fetch2.check_network_access(d, self._buildbzrcommand(ud, d, "revno"), ud.url)
130
131 output = runfetchcmd(self._buildbzrcommand(ud, d, "revno"), d, True)
132
133 return output.strip()
134
135 def sortable_revision(self, url, ud, d, name):
136 """
137 Return a sortable revision number which in our case is the revision number
138 """
139
140 return False, self._build_revision(url, ud, d)
141
142 def _build_revision(self, url, ud, d):
143 return ud.revision
diff --git a/bitbake/lib/bb/fetch2/cvs.py b/bitbake/lib/bb/fetch2/cvs.py
new file mode 100644
index 0000000000..0a672a33ef
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/cvs.py
@@ -0,0 +1,171 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementations
5
6Classes for obtaining upstream sources for the
7BitBake build tools.
8
9"""
10
11# Copyright (C) 2003, 2004 Chris Larson
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26#Based on functions from the base bb module, Copyright 2003 Holger Schurig
27#
28
29import os
30import logging
31import bb
32from bb.fetch2 import FetchMethod, FetchError, MissingParameterError, logger
33from bb.fetch2 import runfetchcmd
34
35class Cvs(FetchMethod):
36 """
37 Class to fetch a module or modules from cvs repositories
38 """
39 def supports(self, url, ud, d):
40 """
41 Check to see if a given url can be fetched with cvs.
42 """
43 return ud.type in ['cvs']
44
45 def urldata_init(self, ud, d):
46 if not "module" in ud.parm:
47 raise MissingParameterError("module", ud.url)
48 ud.module = ud.parm["module"]
49
50 ud.tag = ud.parm.get('tag', "")
51
52 # Override the default date in certain cases
53 if 'date' in ud.parm:
54 ud.date = ud.parm['date']
55 elif ud.tag:
56 ud.date = ""
57
58 norecurse = ''
59 if 'norecurse' in ud.parm:
60 norecurse = '_norecurse'
61
62 fullpath = ''
63 if 'fullpath' in ud.parm:
64 fullpath = '_fullpath'
65
66 ud.localfile = bb.data.expand('%s_%s_%s_%s%s%s.tar.gz' % (ud.module.replace('/', '.'), ud.host, ud.tag, ud.date, norecurse, fullpath), d)
67
68 def need_update(self, url, ud, d):
69 if (ud.date == "now"):
70 return True
71 if not os.path.exists(ud.localpath):
72 return True
73 return False
74
75 def download(self, loc, ud, d):
76
77 method = ud.parm.get('method', 'pserver')
78 localdir = ud.parm.get('localdir', ud.module)
79 cvs_port = ud.parm.get('port', '')
80
81 cvs_rsh = None
82 if method == "ext":
83 if "rsh" in ud.parm:
84 cvs_rsh = ud.parm["rsh"]
85
86 if method == "dir":
87 cvsroot = ud.path
88 else:
89 cvsroot = ":" + method
90 cvsproxyhost = d.getVar('CVS_PROXY_HOST', True)
91 if cvsproxyhost:
92 cvsroot += ";proxy=" + cvsproxyhost
93 cvsproxyport = d.getVar('CVS_PROXY_PORT', True)
94 if cvsproxyport:
95 cvsroot += ";proxyport=" + cvsproxyport
96 cvsroot += ":" + ud.user
97 if ud.pswd:
98 cvsroot += ":" + ud.pswd
99 cvsroot += "@" + ud.host + ":" + cvs_port + ud.path
100
101 options = []
102 if 'norecurse' in ud.parm:
103 options.append("-l")
104 if ud.date:
105 # treat YYYYMMDDHHMM specially for CVS
106 if len(ud.date) == 12:
107 options.append("-D \"%s %s:%s UTC\"" % (ud.date[0:8], ud.date[8:10], ud.date[10:12]))
108 else:
109 options.append("-D \"%s UTC\"" % ud.date)
110 if ud.tag:
111 options.append("-r %s" % ud.tag)
112
113 cvsbasecmd = d.getVar("FETCHCMD_cvs", True)
114 cvscmd = cvsbasecmd + " '-d" + cvsroot + "' co " + " ".join(options) + " " + ud.module
115 cvsupdatecmd = cvsbasecmd + " '-d" + cvsroot + "' update -d -P " + " ".join(options)
116
117 if cvs_rsh:
118 cvscmd = "CVS_RSH=\"%s\" %s" % (cvs_rsh, cvscmd)
119 cvsupdatecmd = "CVS_RSH=\"%s\" %s" % (cvs_rsh, cvsupdatecmd)
120
121 # create module directory
122 logger.debug(2, "Fetch: checking for module directory")
123 pkg = d.getVar('PN', True)
124 pkgdir = os.path.join(d.getVar('CVSDIR', True), pkg)
125 moddir = os.path.join(pkgdir, localdir)
126 if os.access(os.path.join(moddir, 'CVS'), os.R_OK):
127 logger.info("Update " + loc)
128 bb.fetch2.check_network_access(d, cvsupdatecmd, ud.url)
129 # update sources there
130 os.chdir(moddir)
131 cmd = cvsupdatecmd
132 else:
133 logger.info("Fetch " + loc)
134 # check out sources there
135 bb.utils.mkdirhier(pkgdir)
136 os.chdir(pkgdir)
137 logger.debug(1, "Running %s", cvscmd)
138 bb.fetch2.check_network_access(d, cvscmd, ud.url)
139 cmd = cvscmd
140
141 runfetchcmd(cmd, d, cleanup = [moddir])
142
143 if not os.access(moddir, os.R_OK):
144 raise FetchError("Directory %s was not readable despite sucessful fetch?!" % moddir, ud.url)
145
146 scmdata = ud.parm.get("scmdata", "")
147 if scmdata == "keep":
148 tar_flags = ""
149 else:
150 tar_flags = "--exclude 'CVS'"
151
152 # tar them up to a defined filename
153 if 'fullpath' in ud.parm:
154 os.chdir(pkgdir)
155 cmd = "tar %s -czf %s %s" % (tar_flags, ud.localpath, localdir)
156 else:
157 os.chdir(moddir)
158 os.chdir('..')
159 cmd = "tar %s -czf %s %s" % (tar_flags, ud.localpath, os.path.basename(moddir))
160
161 runfetchcmd(cmd, d, cleanup = [ud.localpath])
162
163 def clean(self, ud, d):
164 """ Clean CVS Files and tarballs """
165
166 pkg = d.getVar('PN', True)
167 pkgdir = os.path.join(d.getVar("CVSDIR", True), pkg)
168
169 bb.utils.remove(pkgdir, True)
170 bb.utils.remove(ud.localpath)
171
diff --git a/bitbake/lib/bb/fetch2/git.py b/bitbake/lib/bb/fetch2/git.py
new file mode 100644
index 0000000000..6175e4c7c9
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/git.py
@@ -0,0 +1,326 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' git implementation
5
6git fetcher support the SRC_URI with format of:
7SRC_URI = "git://some.host/somepath;OptionA=xxx;OptionB=xxx;..."
8
9Supported SRC_URI options are:
10
11- branch
12 The git branch to retrieve from. The default is "master"
13
14 This option also supports multiple branch fetching, with branches
15 separated by commas. In multiple branches case, the name option
16 must have the same number of names to match the branches, which is
17 used to specify the SRC_REV for the branch
18 e.g:
19 SRC_URI="git://some.host/somepath;branch=branchX,branchY;name=nameX,nameY"
20 SRCREV_nameX = "xxxxxxxxxxxxxxxxxxxx"
21 SRCREV_nameY = "YYYYYYYYYYYYYYYYYYYY"
22
23- tag
24 The git tag to retrieve. The default is "master"
25
26- protocol
27 The method to use to access the repository. Common options are "git",
28 "http", "https", "file", "ssh" and "rsync". The default is "git".
29
30- rebaseable
31 rebaseable indicates that the upstream git repo may rebase in the future,
32 and current revision may disappear from upstream repo. This option will
33 remind fetcher to preserve local cache carefully for future use.
34 The default value is "0", set rebaseable=1 for rebaseable git repo.
35
36- nocheckout
37 Don't checkout source code when unpacking. set this option for the recipe
38 who has its own routine to checkout code.
39 The default is "0", set nocheckout=1 if needed.
40
41- bareclone
42 Create a bare clone of the source code and don't checkout the source code
43 when unpacking. Set this option for the recipe who has its own routine to
44 checkout code and tracking branch requirements.
45 The default is "0", set bareclone=1 if needed.
46
47"""
48
49#Copyright (C) 2005 Richard Purdie
50#
51# This program is free software; you can redistribute it and/or modify
52# it under the terms of the GNU General Public License version 2 as
53# published by the Free Software Foundation.
54#
55# This program is distributed in the hope that it will be useful,
56# but WITHOUT ANY WARRANTY; without even the implied warranty of
57# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
58# GNU General Public License for more details.
59#
60# You should have received a copy of the GNU General Public License along
61# with this program; if not, write to the Free Software Foundation, Inc.,
62# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
63
64import os
65import bb
66from bb import data
67from bb.fetch2 import FetchMethod
68from bb.fetch2 import runfetchcmd
69from bb.fetch2 import logger
70
71class Git(FetchMethod):
72 """Class to fetch a module or modules from git repositories"""
73 def init(self, d):
74 pass
75
76 def supports(self, url, ud, d):
77 """
78 Check to see if a given url can be fetched with git.
79 """
80 return ud.type in ['git']
81
82 def supports_checksum(self, urldata):
83 return False
84
85 def urldata_init(self, ud, d):
86 """
87 init git specific variable within url data
88 so that the git method like latest_revision() can work
89 """
90 if 'protocol' in ud.parm:
91 ud.proto = ud.parm['protocol']
92 elif not ud.host:
93 ud.proto = 'file'
94 else:
95 ud.proto = "git"
96
97 if not ud.proto in ('git', 'file', 'ssh', 'http', 'https', 'rsync'):
98 raise bb.fetch2.ParameterError("Invalid protocol type", ud.url)
99
100 ud.nocheckout = ud.parm.get("nocheckout","0") == "1"
101
102 ud.rebaseable = ud.parm.get("rebaseable","0") == "1"
103
104 # bareclone implies nocheckout
105 ud.bareclone = ud.parm.get("bareclone","0") == "1"
106 if ud.bareclone:
107 ud.nocheckout = 1
108
109 branches = ud.parm.get("branch", "master").split(',')
110 if len(branches) != len(ud.names):
111 raise bb.fetch2.ParameterError("The number of name and branch parameters is not balanced", ud.url)
112 ud.branches = {}
113 for name in ud.names:
114 branch = branches[ud.names.index(name)]
115 ud.branches[name] = branch
116
117 ud.basecmd = data.getVar("FETCHCMD_git", d, True) or "git"
118
119 ud.write_tarballs = ((data.getVar("BB_GENERATE_MIRROR_TARBALLS", d, True) or "0") != "0") or ud.rebaseable
120
121 ud.setup_revisons(d)
122
123 for name in ud.names:
124 # Ensure anything that doesn't look like a sha256 checksum/revision is translated into one
125 if not ud.revisions[name] or len(ud.revisions[name]) != 40 or (False in [c in "abcdef0123456789" for c in ud.revisions[name]]):
126 if ud.revisions[name]:
127 ud.branches[name] = ud.revisions[name]
128 ud.revisions[name] = self.latest_revision(ud.url, ud, d, name)
129
130 gitsrcname = '%s%s' % (ud.host.replace(':','.'), ud.path.replace('/', '.').replace('*', '.'))
131 # for rebaseable git repo, it is necessary to keep mirror tar ball
132 # per revision, so that even the revision disappears from the
133 # upstream repo in the future, the mirror will remain intact and still
134 # contains the revision
135 if ud.rebaseable:
136 for name in ud.names:
137 gitsrcname = gitsrcname + '_' + ud.revisions[name]
138 ud.mirrortarball = 'git2_%s.tar.gz' % (gitsrcname)
139 ud.fullmirror = os.path.join(d.getVar("DL_DIR", True), ud.mirrortarball)
140 gitdir = d.getVar("GITDIR", True) or (d.getVar("DL_DIR", True) + "/git2/")
141 ud.clonedir = os.path.join(gitdir, gitsrcname)
142
143 ud.localfile = ud.clonedir
144
145 def localpath(self, url, ud, d):
146 return ud.clonedir
147
148 def need_update(self, u, ud, d):
149 if not os.path.exists(ud.clonedir):
150 return True
151 os.chdir(ud.clonedir)
152 for name in ud.names:
153 if not self._contains_ref(ud.revisions[name], d):
154 return True
155 if ud.write_tarballs and not os.path.exists(ud.fullmirror):
156 return True
157 return False
158
159 def try_premirror(self, u, ud, d):
160 # If we don't do this, updating an existing checkout with only premirrors
161 # is not possible
162 if d.getVar("BB_FETCH_PREMIRRORONLY", True) is not None:
163 return True
164 if os.path.exists(ud.clonedir):
165 return False
166 return True
167
168 def download(self, loc, ud, d):
169 """Fetch url"""
170
171 if ud.user:
172 username = ud.user + '@'
173 else:
174 username = ""
175
176 ud.repochanged = not os.path.exists(ud.fullmirror)
177
178 # If the checkout doesn't exist and the mirror tarball does, extract it
179 if not os.path.exists(ud.clonedir) and os.path.exists(ud.fullmirror):
180 bb.utils.mkdirhier(ud.clonedir)
181 os.chdir(ud.clonedir)
182 runfetchcmd("tar -xzf %s" % (ud.fullmirror), d)
183
184 repourl = "%s://%s%s%s" % (ud.proto, username, ud.host, ud.path)
185
186 # If the repo still doesn't exist, fallback to cloning it
187 if not os.path.exists(ud.clonedir):
188 # We do this since git will use a "-l" option automatically for local urls where possible
189 if repourl.startswith("file://"):
190 repourl = repourl[7:]
191 clone_cmd = "%s clone --bare --mirror %s %s" % (ud.basecmd, repourl, ud.clonedir)
192 if ud.proto.lower() != 'file':
193 bb.fetch2.check_network_access(d, clone_cmd)
194 runfetchcmd(clone_cmd, d)
195
196 os.chdir(ud.clonedir)
197 # Update the checkout if needed
198 needupdate = False
199 for name in ud.names:
200 if not self._contains_ref(ud.revisions[name], d):
201 needupdate = True
202 if needupdate:
203 try:
204 runfetchcmd("%s remote rm origin" % ud.basecmd, d)
205 except bb.fetch2.FetchError:
206 logger.debug(1, "No Origin")
207
208 runfetchcmd("%s remote add --mirror=fetch origin %s" % (ud.basecmd, repourl), d)
209 fetch_cmd = "%s fetch -f --prune %s refs/*:refs/*" % (ud.basecmd, repourl)
210 if ud.proto.lower() != 'file':
211 bb.fetch2.check_network_access(d, fetch_cmd, ud.url)
212 runfetchcmd(fetch_cmd, d)
213 runfetchcmd("%s prune-packed" % ud.basecmd, d)
214 runfetchcmd("%s pack-redundant --all | xargs -r rm" % ud.basecmd, d)
215 ud.repochanged = True
216
217 def build_mirror_data(self, url, ud, d):
218 # Generate a mirror tarball if needed
219 if ud.write_tarballs and (ud.repochanged or not os.path.exists(ud.fullmirror)):
220 # it's possible that this symlink points to read-only filesystem with PREMIRROR
221 if os.path.islink(ud.fullmirror):
222 os.unlink(ud.fullmirror)
223
224 os.chdir(ud.clonedir)
225 logger.info("Creating tarball of git repository")
226 runfetchcmd("tar -czf %s %s" % (ud.fullmirror, os.path.join(".") ), d)
227 runfetchcmd("touch %s.done" % (ud.fullmirror), d)
228
229 def unpack(self, ud, destdir, d):
230 """ unpack the downloaded src to destdir"""
231
232 subdir = ud.parm.get("subpath", "")
233 if subdir != "":
234 readpathspec = ":%s" % (subdir)
235 def_destsuffix = "%s/" % os.path.basename(subdir)
236 else:
237 readpathspec = ""
238 def_destsuffix = "git/"
239
240 destsuffix = ud.parm.get("destsuffix", def_destsuffix)
241 destdir = ud.destdir = os.path.join(destdir, destsuffix)
242 if os.path.exists(destdir):
243 bb.utils.prunedir(destdir)
244
245 cloneflags = "-s -n"
246 if ud.bareclone:
247 cloneflags += " --mirror"
248
249 # Versions of git prior to 1.7.9.2 have issues where foo.git and foo get confused
250 # and you end up with some horrible union of the two when you attempt to clone it
251 # The least invasive workaround seems to be a symlink to the real directory to
252 # fool git into ignoring any .git version that may also be present.
253 #
254 # The issue is fixed in more recent versions of git so we can drop this hack in future
255 # when that version becomes common enough.
256 clonedir = ud.clonedir
257 if not ud.path.endswith(".git"):
258 indirectiondir = destdir[:-1] + ".indirectionsymlink"
259 if os.path.exists(indirectiondir):
260 os.remove(indirectiondir)
261 bb.utils.mkdirhier(os.path.dirname(indirectiondir))
262 os.symlink(ud.clonedir, indirectiondir)
263 clonedir = indirectiondir
264
265 runfetchcmd("git clone %s %s/ %s" % (cloneflags, clonedir, destdir), d)
266 if not ud.nocheckout:
267 os.chdir(destdir)
268 if subdir != "":
269 runfetchcmd("%s read-tree %s%s" % (ud.basecmd, ud.revisions[ud.names[0]], readpathspec), d)
270 runfetchcmd("%s checkout-index -q -f -a" % ud.basecmd, d)
271 else:
272 runfetchcmd("%s checkout %s" % (ud.basecmd, ud.revisions[ud.names[0]]), d)
273 return True
274
275 def clean(self, ud, d):
276 """ clean the git directory """
277
278 bb.utils.remove(ud.localpath, True)
279 bb.utils.remove(ud.fullmirror)
280
281 def supports_srcrev(self):
282 return True
283
284 def _contains_ref(self, tag, d):
285 basecmd = data.getVar("FETCHCMD_git", d, True) or "git"
286 cmd = "%s log --pretty=oneline -n 1 %s -- 2> /dev/null | wc -l" % (basecmd, tag)
287 output = runfetchcmd(cmd, d, quiet=True)
288 if len(output.split()) > 1:
289 raise bb.fetch2.FetchError("The command '%s' gave output with more then 1 line unexpectedly, output: '%s'" % (cmd, output))
290 return output.split()[0] != "0"
291
292 def _revision_key(self, url, ud, d, name):
293 """
294 Return a unique key for the url
295 """
296 return "git:" + ud.host + ud.path.replace('/', '.') + ud.branches[name]
297
298 def _latest_revision(self, url, ud, d, name):
299 """
300 Compute the HEAD revision for the url
301 """
302 if ud.user:
303 username = ud.user + '@'
304 else:
305 username = ""
306
307 basecmd = data.getVar("FETCHCMD_git", d, True) or "git"
308 cmd = "%s ls-remote %s://%s%s%s %s" % \
309 (basecmd, ud.proto, username, ud.host, ud.path, ud.branches[name])
310 if ud.proto.lower() != 'file':
311 bb.fetch2.check_network_access(d, cmd)
312 output = runfetchcmd(cmd, d, True)
313 if not output:
314 raise bb.fetch2.FetchError("The command %s gave empty output unexpectedly" % cmd, url)
315 return output.split()[0]
316
317 def _build_revision(self, url, ud, d, name):
318 return ud.revisions[name]
319
320 def checkstatus(self, uri, ud, d):
321 fetchcmd = "%s ls-remote %s" % (ud.basecmd, uri)
322 try:
323 runfetchcmd(fetchcmd, d, quiet=True)
324 return True
325 except FetchError:
326 return False
diff --git a/bitbake/lib/bb/fetch2/gitsm.py b/bitbake/lib/bb/fetch2/gitsm.py
new file mode 100644
index 0000000000..572b637c9a
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/gitsm.py
@@ -0,0 +1,78 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' git submodules implementation
5"""
6
7# Copyright (C) 2013 Richard Purdie
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22import os
23import bb
24from bb import data
25from bb.fetch2.git import Git
26from bb.fetch2 import runfetchcmd
27from bb.fetch2 import logger
28
29class GitSM(Git):
30 def supports(self, url, ud, d):
31 """
32 Check to see if a given url can be fetched with git.
33 """
34 return ud.type in ['gitsm']
35
36 def uses_submodules(self, ud, d):
37 for name in ud.names:
38 try:
39 runfetchcmd("%s show %s:.gitmodules" % (ud.basecmd, ud.revisions[name]), d, quiet=True)
40 return True
41 except bb.fetch.FetchError:
42 pass
43 return False
44
45 def update_submodules(self, u, ud, d):
46 # We have to convert bare -> full repo, do the submodule bit, then convert back
47 tmpclonedir = ud.clonedir + ".tmp"
48 gitdir = tmpclonedir + os.sep + ".git"
49 bb.utils.remove(tmpclonedir, True)
50 os.mkdir(tmpclonedir)
51 os.rename(ud.clonedir, gitdir)
52 runfetchcmd("sed " + gitdir + "/config -i -e 's/bare.*=.*true/bare = false/'", d)
53 os.chdir(tmpclonedir)
54 runfetchcmd("git reset --hard", d)
55 runfetchcmd("git submodule init", d)
56 runfetchcmd("git submodule update", d)
57 runfetchcmd("sed " + gitdir + "/config -i -e 's/bare.*=.*false/bare = true/'", d)
58 os.rename(gitdir, ud.clonedir,)
59 bb.utils.remove(tmpclonedir, True)
60
61 def download(self, loc, ud, d):
62 Git.download(self, loc, ud, d)
63
64 os.chdir(ud.clonedir)
65 submodules = self.uses_submodules(ud, d)
66 if submodules:
67 self.update_submodules(loc, ud, d)
68
69 def unpack(self, ud, destdir, d):
70 Git.unpack(self, ud, destdir, d)
71
72 os.chdir(ud.destdir)
73 submodules = self.uses_submodules(ud, d)
74 if submodules:
75 runfetchcmd("cp -r " + ud.clonedir + "/modules " + ud.destdir + "/.git/", d)
76 runfetchcmd("git submodule init", d)
77 runfetchcmd("git submodule update", d)
78
diff --git a/bitbake/lib/bb/fetch2/hg.py b/bitbake/lib/bb/fetch2/hg.py
new file mode 100644
index 0000000000..b1c8675dd4
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/hg.py
@@ -0,0 +1,181 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementation for mercurial DRCS (hg).
5
6"""
7
8# Copyright (C) 2003, 2004 Chris Larson
9# Copyright (C) 2004 Marcin Juszkiewicz
10# Copyright (C) 2007 Robert Schuster
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License version 2 as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along
22# with this program; if not, write to the Free Software Foundation, Inc.,
23# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24#
25# Based on functions from the base bb module, Copyright 2003 Holger Schurig
26
27import os
28import sys
29import logging
30import bb
31from bb import data
32from bb.fetch2 import FetchMethod
33from bb.fetch2 import FetchError
34from bb.fetch2 import MissingParameterError
35from bb.fetch2 import runfetchcmd
36from bb.fetch2 import logger
37
38class Hg(FetchMethod):
39 """Class to fetch from mercurial repositories"""
40 def supports(self, url, ud, d):
41 """
42 Check to see if a given url can be fetched with mercurial.
43 """
44 return ud.type in ['hg']
45
46 def urldata_init(self, ud, d):
47 """
48 init hg specific variable within url data
49 """
50 if not "module" in ud.parm:
51 raise MissingParameterError('module', ud.url)
52
53 ud.module = ud.parm["module"]
54
55 # Create paths to mercurial checkouts
56 relpath = self._strip_leading_slashes(ud.path)
57 ud.pkgdir = os.path.join(data.expand('${HGDIR}', d), ud.host, relpath)
58 ud.moddir = os.path.join(ud.pkgdir, ud.module)
59
60 ud.setup_revisons(d)
61
62 if 'rev' in ud.parm:
63 ud.revision = ud.parm['rev']
64 elif not ud.revision:
65 ud.revision = self.latest_revision(ud.url, ud, d)
66
67 ud.localfile = data.expand('%s_%s_%s_%s.tar.gz' % (ud.module.replace('/', '.'), ud.host, ud.path.replace('/', '.'), ud.revision), d)
68
69 def need_update(self, url, ud, d):
70 revTag = ud.parm.get('rev', 'tip')
71 if revTag == "tip":
72 return True
73 if not os.path.exists(ud.localpath):
74 return True
75 return False
76
77 def _buildhgcommand(self, ud, d, command):
78 """
79 Build up an hg commandline based on ud
80 command is "fetch", "update", "info"
81 """
82
83 basecmd = data.expand('${FETCHCMD_hg}', d)
84
85 proto = ud.parm.get('protocol', 'http')
86
87 host = ud.host
88 if proto == "file":
89 host = "/"
90 ud.host = "localhost"
91
92 if not ud.user:
93 hgroot = host + ud.path
94 else:
95 hgroot = ud.user + "@" + host + ud.path
96
97 if command == "info":
98 return "%s identify -i %s://%s/%s" % (basecmd, proto, hgroot, ud.module)
99
100 options = [];
101
102 # Don't specify revision for the fetch; clone the entire repo.
103 # This avoids an issue if the specified revision is a tag, because
104 # the tag actually exists in the specified revision + 1, so it won't
105 # be available when used in any successive commands.
106 if ud.revision and command != "fetch":
107 options.append("-r %s" % ud.revision)
108
109 if command == "fetch":
110 cmd = "%s clone %s %s://%s/%s %s" % (basecmd, " ".join(options), proto, hgroot, ud.module, ud.module)
111 elif command == "pull":
112 # do not pass options list; limiting pull to rev causes the local
113 # repo not to contain it and immediately following "update" command
114 # will crash
115 cmd = "%s pull" % (basecmd)
116 elif command == "update":
117 cmd = "%s update -C %s" % (basecmd, " ".join(options))
118 else:
119 raise FetchError("Invalid hg command %s" % command, ud.url)
120
121 return cmd
122
123 def download(self, loc, ud, d):
124 """Fetch url"""
125
126 logger.debug(2, "Fetch: checking for module directory '" + ud.moddir + "'")
127
128 if os.access(os.path.join(ud.moddir, '.hg'), os.R_OK):
129 updatecmd = self._buildhgcommand(ud, d, "pull")
130 logger.info("Update " + loc)
131 # update sources there
132 os.chdir(ud.moddir)
133 logger.debug(1, "Running %s", updatecmd)
134 bb.fetch2.check_network_access(d, updatecmd, ud.url)
135 runfetchcmd(updatecmd, d)
136
137 else:
138 fetchcmd = self._buildhgcommand(ud, d, "fetch")
139 logger.info("Fetch " + loc)
140 # check out sources there
141 bb.utils.mkdirhier(ud.pkgdir)
142 os.chdir(ud.pkgdir)
143 logger.debug(1, "Running %s", fetchcmd)
144 bb.fetch2.check_network_access(d, fetchcmd, ud.url)
145 runfetchcmd(fetchcmd, d)
146
147 # Even when we clone (fetch), we still need to update as hg's clone
148 # won't checkout the specified revision if its on a branch
149 updatecmd = self._buildhgcommand(ud, d, "update")
150 os.chdir(ud.moddir)
151 logger.debug(1, "Running %s", updatecmd)
152 runfetchcmd(updatecmd, d)
153
154 scmdata = ud.parm.get("scmdata", "")
155 if scmdata == "keep":
156 tar_flags = ""
157 else:
158 tar_flags = "--exclude '.hg' --exclude '.hgrags'"
159
160 os.chdir(ud.pkgdir)
161 runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, ud.module), d, cleanup = [ud.localpath])
162
163 def supports_srcrev(self):
164 return True
165
166 def _latest_revision(self, url, ud, d, name):
167 """
168 Compute tip revision for the url
169 """
170 bb.fetch2.check_network_access(d, self._buildhgcommand(ud, d, "info"))
171 output = runfetchcmd(self._buildhgcommand(ud, d, "info"), d)
172 return output.strip()
173
174 def _build_revision(self, url, ud, d, name):
175 return ud.revision
176
177 def _revision_key(self, url, ud, d, name):
178 """
179 Return a unique key for the url
180 """
181 return "hg:" + ud.moddir
diff --git a/bitbake/lib/bb/fetch2/local.py b/bitbake/lib/bb/fetch2/local.py
new file mode 100644
index 0000000000..58bbe20327
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/local.py
@@ -0,0 +1,116 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementations
5
6Classes for obtaining upstream sources for the
7BitBake build tools.
8
9"""
10
11# Copyright (C) 2003, 2004 Chris Larson
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26# Based on functions from the base bb module, Copyright 2003 Holger Schurig
27
28import os
29import urllib
30import bb
31import bb.utils
32from bb import data
33from bb.fetch2 import FetchMethod, FetchError
34from bb.fetch2 import logger
35
36class Local(FetchMethod):
37 def supports(self, url, urldata, d):
38 """
39 Check to see if a given url represents a local fetch.
40 """
41 return urldata.type in ['file']
42
43 def urldata_init(self, ud, d):
44 # We don't set localfile as for this fetcher the file is already local!
45 ud.decodedurl = urllib.unquote(ud.url.split("://")[1].split(";")[0])
46 ud.basename = os.path.basename(ud.decodedurl)
47 ud.basepath = ud.decodedurl
48 return
49
50 def localpath(self, url, urldata, d):
51 """
52 Return the local filename of a given url assuming a successful fetch.
53 """
54 path = urldata.decodedurl
55 newpath = path
56 if path[0] != "/":
57 filespath = data.getVar('FILESPATH', d, True)
58 if filespath:
59 logger.debug(2, "Searching for %s in paths: \n%s" % (path, "\n ".join(filespath.split(":"))))
60 newpath = bb.utils.which(filespath, path)
61 if not newpath:
62 filesdir = data.getVar('FILESDIR', d, True)
63 if filesdir:
64 logger.debug(2, "Searching for %s in path: %s" % (path, filesdir))
65 newpath = os.path.join(filesdir, path)
66 if (not newpath or not os.path.exists(newpath)) and path.find("*") != -1:
67 # For expressions using '*', best we can do is take the first directory in FILESPATH that exists
68 newpath = bb.utils.which(filespath, ".")
69 logger.debug(2, "Searching for %s in path: %s" % (path, newpath))
70 return newpath
71 if not os.path.exists(newpath):
72 dldirfile = os.path.join(d.getVar("DL_DIR", True), path)
73 logger.debug(2, "Defaulting to %s for %s" % (dldirfile, path))
74 bb.utils.mkdirhier(os.path.dirname(dldirfile))
75 return dldirfile
76 return newpath
77
78 def need_update(self, url, ud, d):
79 if url.find("*") != -1:
80 return False
81 if os.path.exists(ud.localpath):
82 return False
83 return True
84
85 def download(self, url, urldata, d):
86 """Fetch urls (no-op for Local method)"""
87 # no need to fetch local files, we'll deal with them in place.
88 if self.supports_checksum(urldata) and not os.path.exists(urldata.localpath):
89 locations = []
90 filespath = data.getVar('FILESPATH', d, True)
91 if filespath:
92 locations = filespath.split(":")
93 filesdir = data.getVar('FILESDIR', d, True)
94 if filesdir:
95 locations.append(filesdir)
96 locations.append(d.getVar("DL_DIR", True))
97
98 msg = "Unable to find file " + url + " anywhere. The paths that were searched were:\n " + "\n ".join(locations)
99 raise FetchError(msg)
100
101 return True
102
103 def checkstatus(self, url, urldata, d):
104 """
105 Check the status of the url
106 """
107 if urldata.localpath.find("*") != -1:
108 logger.info("URL %s looks like a glob and was therefore not checked.", url)
109 return True
110 if os.path.exists(urldata.localpath):
111 return True
112 return False
113
114 def clean(self, urldata, d):
115 return
116
diff --git a/bitbake/lib/bb/fetch2/osc.py b/bitbake/lib/bb/fetch2/osc.py
new file mode 100644
index 0000000000..1a3a7bb56b
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/osc.py
@@ -0,0 +1,135 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4Bitbake "Fetch" implementation for osc (Opensuse build service client).
5Based on the svn "Fetch" implementation.
6
7"""
8
9import os
10import sys
11import logging
12import bb
13from bb import data
14from bb.fetch2 import FetchMethod
15from bb.fetch2 import FetchError
16from bb.fetch2 import MissingParameterError
17from bb.fetch2 import runfetchcmd
18
19class Osc(FetchMethod):
20 """Class to fetch a module or modules from Opensuse build server
21 repositories."""
22
23 def supports(self, url, ud, d):
24 """
25 Check to see if a given url can be fetched with osc.
26 """
27 return ud.type in ['osc']
28
29 def urldata_init(self, ud, d):
30 if not "module" in ud.parm:
31 raise MissingParameterError('module', ud.url)
32
33 ud.module = ud.parm["module"]
34
35 # Create paths to osc checkouts
36 relpath = self._strip_leading_slashes(ud.path)
37 ud.pkgdir = os.path.join(data.expand('${OSCDIR}', d), ud.host)
38 ud.moddir = os.path.join(ud.pkgdir, relpath, ud.module)
39
40 if 'rev' in ud.parm:
41 ud.revision = ud.parm['rev']
42 else:
43 pv = data.getVar("PV", d, 0)
44 rev = bb.fetch2.srcrev_internal_helper(ud, d)
45 if rev and rev != True:
46 ud.revision = rev
47 else:
48 ud.revision = ""
49
50 ud.localfile = data.expand('%s_%s_%s.tar.gz' % (ud.module.replace('/', '.'), ud.path.replace('/', '.'), ud.revision), d)
51
52 def _buildosccommand(self, ud, d, command):
53 """
54 Build up an ocs commandline based on ud
55 command is "fetch", "update", "info"
56 """
57
58 basecmd = data.expand('${FETCHCMD_osc}', d)
59
60 proto = ud.parm.get('protocol', 'ocs')
61
62 options = []
63
64 config = "-c %s" % self.generate_config(ud, d)
65
66 if ud.revision:
67 options.append("-r %s" % ud.revision)
68
69 coroot = self._strip_leading_slashes(ud.path)
70
71 if command == "fetch":
72 osccmd = "%s %s co %s/%s %s" % (basecmd, config, coroot, ud.module, " ".join(options))
73 elif command == "update":
74 osccmd = "%s %s up %s" % (basecmd, config, " ".join(options))
75 else:
76 raise FetchError("Invalid osc command %s" % command, ud.url)
77
78 return osccmd
79
80 def download(self, loc, ud, d):
81 """
82 Fetch url
83 """
84
85 logger.debug(2, "Fetch: checking for module directory '" + ud.moddir + "'")
86
87 if os.access(os.path.join(data.expand('${OSCDIR}', d), ud.path, ud.module), os.R_OK):
88 oscupdatecmd = self._buildosccommand(ud, d, "update")
89 logger.info("Update "+ loc)
90 # update sources there
91 os.chdir(ud.moddir)
92 logger.debug(1, "Running %s", oscupdatecmd)
93 bb.fetch2.check_network_access(d, oscupdatecmd, ud.url)
94 runfetchcmd(oscupdatecmd, d)
95 else:
96 oscfetchcmd = self._buildosccommand(ud, d, "fetch")
97 logger.info("Fetch " + loc)
98 # check out sources there
99 bb.utils.mkdirhier(ud.pkgdir)
100 os.chdir(ud.pkgdir)
101 logger.debug(1, "Running %s", oscfetchcmd)
102 bb.fetch2.check_network_access(d, oscfetchcmd, ud.url)
103 runfetchcmd(oscfetchcmd, d)
104
105 os.chdir(os.path.join(ud.pkgdir + ud.path))
106 # tar them up to a defined filename
107 runfetchcmd("tar -czf %s %s" % (ud.localpath, ud.module), d, cleanup = [ud.localpath])
108
109 def supports_srcrev(self):
110 return False
111
112 def generate_config(self, ud, d):
113 """
114 Generate a .oscrc to be used for this run.
115 """
116
117 config_path = os.path.join(data.expand('${OSCDIR}', d), "oscrc")
118 if (os.path.exists(config_path)):
119 os.remove(config_path)
120
121 f = open(config_path, 'w')
122 f.write("[general]\n")
123 f.write("apisrv = %s\n" % ud.host)
124 f.write("scheme = http\n")
125 f.write("su-wrapper = su -c\n")
126 f.write("build-root = %s\n" % data.expand('${WORKDIR}', d))
127 f.write("urllist = http://moblin-obs.jf.intel.com:8888/build/%(project)s/%(repository)s/%(buildarch)s/:full/%(name)s.rpm\n")
128 f.write("extra-pkgs = gzip\n")
129 f.write("\n")
130 f.write("[%s]\n" % ud.host)
131 f.write("user = %s\n" % ud.parm["user"])
132 f.write("pass = %s\n" % ud.parm["pswd"])
133 f.close()
134
135 return config_path
diff --git a/bitbake/lib/bb/fetch2/perforce.py b/bitbake/lib/bb/fetch2/perforce.py
new file mode 100644
index 0000000000..fc4074d5a3
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/perforce.py
@@ -0,0 +1,198 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementations
5
6Classes for obtaining upstream sources for the
7BitBake build tools.
8
9"""
10
11# Copyright (C) 2003, 2004 Chris Larson
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26# Based on functions from the base bb module, Copyright 2003 Holger Schurig
27
28from future_builtins import zip
29import os
30import subprocess
31import logging
32import bb
33from bb import data
34from bb.fetch2 import FetchMethod
35from bb.fetch2 import FetchError
36from bb.fetch2 import logger
37from bb.fetch2 import runfetchcmd
38
39class Perforce(FetchMethod):
40 def supports(self, url, ud, d):
41 return ud.type in ['p4']
42
43 def doparse(url, d):
44 parm = {}
45 path = url.split("://")[1]
46 delim = path.find("@");
47 if delim != -1:
48 (user, pswd, host, port) = path.split('@')[0].split(":")
49 path = path.split('@')[1]
50 else:
51 (host, port) = data.getVar('P4PORT', d).split(':')
52 user = ""
53 pswd = ""
54
55 if path.find(";") != -1:
56 keys=[]
57 values=[]
58 plist = path.split(';')
59 for item in plist:
60 if item.count('='):
61 (key, value) = item.split('=')
62 keys.append(key)
63 values.append(value)
64
65 parm = dict(zip(keys, values))
66 path = "//" + path.split(';')[0]
67 host += ":%s" % (port)
68 parm["cset"] = Perforce.getcset(d, path, host, user, pswd, parm)
69
70 return host, path, user, pswd, parm
71 doparse = staticmethod(doparse)
72
73 def getcset(d, depot, host, user, pswd, parm):
74 p4opt = ""
75 if "cset" in parm:
76 return parm["cset"];
77 if user:
78 p4opt += " -u %s" % (user)
79 if pswd:
80 p4opt += " -P %s" % (pswd)
81 if host:
82 p4opt += " -p %s" % (host)
83
84 p4date = data.getVar("P4DATE", d, True)
85 if "revision" in parm:
86 depot += "#%s" % (parm["revision"])
87 elif "label" in parm:
88 depot += "@%s" % (parm["label"])
89 elif p4date:
90 depot += "@%s" % (p4date)
91
92 p4cmd = data.getVar('FETCHCOMMAND_p4', d, True)
93 logger.debug(1, "Running %s%s changes -m 1 %s", p4cmd, p4opt, depot)
94 p4file, errors = bb.process.run("%s%s changes -m 1 %s" % (p4cmd, p4opt, depot))
95 cset = p4file.strip()
96 logger.debug(1, "READ %s", cset)
97 if not cset:
98 return -1
99
100 return cset.split(' ')[1]
101 getcset = staticmethod(getcset)
102
103 def urldata_init(self, ud, d):
104 (host, path, user, pswd, parm) = Perforce.doparse(ud.url, d)
105
106 # If a label is specified, we use that as our filename
107
108 if "label" in parm:
109 ud.localfile = "%s.tar.gz" % (parm["label"])
110 return
111
112 base = path
113 which = path.find('/...')
114 if which != -1:
115 base = path[:which]
116
117 base = self._strip_leading_slashes(base)
118
119 cset = Perforce.getcset(d, path, host, user, pswd, parm)
120
121 ud.localfile = data.expand('%s+%s+%s.tar.gz' % (host, base.replace('/', '.'), cset), d)
122
123 def download(self, loc, ud, d):
124 """
125 Fetch urls
126 """
127
128 (host, depot, user, pswd, parm) = Perforce.doparse(loc, d)
129
130 if depot.find('/...') != -1:
131 path = depot[:depot.find('/...')]
132 else:
133 path = depot
134
135 module = parm.get('module', os.path.basename(path))
136
137 localdata = data.createCopy(d)
138 data.setVar('OVERRIDES', "p4:%s" % data.getVar('OVERRIDES', localdata), localdata)
139 data.update_data(localdata)
140
141 # Get the p4 command
142 p4opt = ""
143 if user:
144 p4opt += " -u %s" % (user)
145
146 if pswd:
147 p4opt += " -P %s" % (pswd)
148
149 if host:
150 p4opt += " -p %s" % (host)
151
152 p4cmd = data.getVar('FETCHCOMMAND', localdata, True)
153
154 # create temp directory
155 logger.debug(2, "Fetch: creating temporary directory")
156 bb.utils.mkdirhier(data.expand('${WORKDIR}', localdata))
157 data.setVar('TMPBASE', data.expand('${WORKDIR}/oep4.XXXXXX', localdata), localdata)
158 tmpfile, errors = bb.process.run(data.getVar('MKTEMPDIRCMD', localdata, True) or "false")
159 tmpfile = tmpfile.strip()
160 if not tmpfile:
161 raise FetchError("Fetch: unable to create temporary directory.. make sure 'mktemp' is in the PATH.", loc)
162
163 if "label" in parm:
164 depot = "%s@%s" % (depot, parm["label"])
165 else:
166 cset = Perforce.getcset(d, depot, host, user, pswd, parm)
167 depot = "%s@%s" % (depot, cset)
168
169 os.chdir(tmpfile)
170 logger.info("Fetch " + loc)
171 logger.info("%s%s files %s", p4cmd, p4opt, depot)
172 p4file, errors = bb.process.run("%s%s files %s" % (p4cmd, p4opt, depot))
173 p4file = [f.rstrip() for f in p4file.splitlines()]
174
175 if not p4file:
176 raise FetchError("Fetch: unable to get the P4 files from %s" % depot, loc)
177
178 count = 0
179
180 for file in p4file:
181 list = file.split()
182
183 if list[2] == "delete":
184 continue
185
186 dest = list[0][len(path)+1:]
187 where = dest.find("#")
188
189 subprocess.call("%s%s print -o %s/%s %s" % (p4cmd, p4opt, module, dest[:where], list[0]), shell=True)
190 count = count + 1
191
192 if count == 0:
193 logger.error()
194 raise FetchError("Fetch: No files gathered from the P4 fetch", loc)
195
196 runfetchcmd("tar -czf %s %s" % (ud.localpath, module), d, cleanup = [ud.localpath])
197 # cleanup
198 bb.utils.prunedir(tmpfile)
diff --git a/bitbake/lib/bb/fetch2/repo.py b/bitbake/lib/bb/fetch2/repo.py
new file mode 100644
index 0000000000..8300da8c5a
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/repo.py
@@ -0,0 +1,98 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake "Fetch" repo (git) implementation
5
6"""
7
8# Copyright (C) 2009 Tom Rini <trini@embeddedalley.com>
9#
10# Based on git.py which is:
11#Copyright (C) 2005 Richard Purdie
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25
26import os
27import bb
28from bb import data
29from bb.fetch2 import FetchMethod
30from bb.fetch2 import runfetchcmd
31
32class Repo(FetchMethod):
33 """Class to fetch a module or modules from repo (git) repositories"""
34 def supports(self, url, ud, d):
35 """
36 Check to see if a given url can be fetched with repo.
37 """
38 return ud.type in ["repo"]
39
40 def urldata_init(self, ud, d):
41 """
42 We don"t care about the git rev of the manifests repository, but
43 we do care about the manifest to use. The default is "default".
44 We also care about the branch or tag to be used. The default is
45 "master".
46 """
47
48 ud.proto = ud.parm.get('protocol', 'git')
49 ud.branch = ud.parm.get('branch', 'master')
50 ud.manifest = ud.parm.get('manifest', 'default.xml')
51 if not ud.manifest.endswith('.xml'):
52 ud.manifest += '.xml'
53
54 ud.localfile = data.expand("repo_%s%s_%s_%s.tar.gz" % (ud.host, ud.path.replace("/", "."), ud.manifest, ud.branch), d)
55
56 def download(self, loc, ud, d):
57 """Fetch url"""
58
59 if os.access(os.path.join(data.getVar("DL_DIR", d, True), ud.localfile), os.R_OK):
60 logger.debug(1, "%s already exists (or was stashed). Skipping repo init / sync.", ud.localpath)
61 return
62
63 gitsrcname = "%s%s" % (ud.host, ud.path.replace("/", "."))
64 repodir = data.getVar("REPODIR", d, True) or os.path.join(data.getVar("DL_DIR", d, True), "repo")
65 codir = os.path.join(repodir, gitsrcname, ud.manifest)
66
67 if ud.user:
68 username = ud.user + "@"
69 else:
70 username = ""
71
72 bb.utils.mkdirhier(os.path.join(codir, "repo"))
73 os.chdir(os.path.join(codir, "repo"))
74 if not os.path.exists(os.path.join(codir, "repo", ".repo")):
75 bb.fetch2.check_network_access(d, "repo init -m %s -b %s -u %s://%s%s%s" % (ud.manifest, ud.branch, ud.proto, username, ud.host, ud.path), ud.url)
76 runfetchcmd("repo init -m %s -b %s -u %s://%s%s%s" % (ud.manifest, ud.branch, ud.proto, username, ud.host, ud.path), d)
77
78 bb.fetch2.check_network_access(d, "repo sync %s" % ud.url, ud.url)
79 runfetchcmd("repo sync", d)
80 os.chdir(codir)
81
82 scmdata = ud.parm.get("scmdata", "")
83 if scmdata == "keep":
84 tar_flags = ""
85 else:
86 tar_flags = "--exclude '.repo' --exclude '.git'"
87
88 # Create a cache
89 runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, os.path.join(".", "*") ), d)
90
91 def supports_srcrev(self):
92 return False
93
94 def _build_revision(self, url, ud, d):
95 return ud.manifest
96
97 def _want_sortable_revision(self, url, ud, d):
98 return False
diff --git a/bitbake/lib/bb/fetch2/sftp.py b/bitbake/lib/bb/fetch2/sftp.py
new file mode 100644
index 0000000000..5fbbcfdd90
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/sftp.py
@@ -0,0 +1,129 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake SFTP Fetch implementation
5
6Class for fetching files via SFTP. It tries to adhere to the (now
7expired) IETF Internet Draft for "Uniform Resource Identifier (URI)
8Scheme for Secure File Transfer Protocol (SFTP) and Secure Shell
9(SSH)" (SECSH URI).
10
11It uses SFTP (as to adhere to the SECSH URI specification). It only
12supports key based authentication, not password. This class, unlike
13the SSH fetcher, does not support fetching a directory tree from the
14remote.
15
16 http://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04
17 https://www.iana.org/assignments/uri-schemes/prov/sftp
18 https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
19
20Please note that '/' is used as host path seperator, and not ":"
21as you may be used to from the scp/sftp commands. You can use a
22~ (tilde) to specify a path relative to your home directory.
23(The /~user/ syntax, for specyfing a path relative to another
24user's home directory is not supported.) Note that the tilde must
25still follow the host path seperator ("/"). See exampels below.
26
27Example SRC_URIs:
28
29SRC_URI = "sftp://host.example.com/dir/path.file.txt"
30
31A path relative to your home directory.
32
33SRC_URI = "sftp://host.example.com/~/dir/path.file.txt"
34
35You can also specify a username (specyfing password in the
36URI is not supported, use SSH keys to authenticate):
37
38SRC_URI = "sftp://user@host.example.com/dir/path.file.txt"
39
40"""
41
42# Copyright (C) 2013, Olof Johansson <olof.johansson@axis.com>
43#
44# Based in part on bb.fetch2.wget:
45# Copyright (C) 2003, 2004 Chris Larson
46#
47# This program is free software; you can redistribute it and/or modify
48# it under the terms of the GNU General Public License version 2 as
49# published by the Free Software Foundation.
50#
51# This program is distributed in the hope that it will be useful,
52# but WITHOUT ANY WARRANTY; without even the implied warranty of
53# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
54# GNU General Public License for more details.
55#
56# You should have received a copy of the GNU General Public License along
57# with this program; if not, write to the Free Software Foundation, Inc.,
58# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
59#
60# Based on functions from the base bb module, Copyright 2003 Holger Schurig
61
62import os
63import bb
64import urllib
65import commands
66from bb import data
67from bb.fetch2 import URI
68from bb.fetch2 import FetchMethod
69from bb.fetch2 import runfetchcmd
70
71
72class SFTP(FetchMethod):
73 """Class to fetch urls via 'sftp'"""
74
75 def supports(self, url, ud, d):
76 """
77 Check to see if a given url can be fetched with sftp.
78 """
79 return ud.type in ['sftp']
80
81 def recommends_checksum(self, urldata):
82 return True
83
84 def urldata_init(self, ud, d):
85 if 'protocol' in ud.parm and ud.parm['protocol'] == 'git':
86 raise bb.fetch2.ParameterError(
87 "Invalid protocol - if you wish to fetch from a " +
88 "git repository using ssh, you need to use the " +
89 "git:// prefix with protocol=ssh", ud.url)
90
91 if 'downloadfilename' in ud.parm:
92 ud.basename = ud.parm['downloadfilename']
93 else:
94 ud.basename = os.path.basename(ud.path)
95
96 ud.localfile = data.expand(urllib.unquote(ud.basename), d)
97
98 def download(self, uri, ud, d):
99 """Fetch urls"""
100
101 urlo = URI(uri)
102 basecmd = 'sftp -oPasswordAuthentication=no'
103 port = ''
104 if urlo.port:
105 port = '-P %d' % urlo.port
106 urlo.port = None
107
108 dldir = data.getVar('DL_DIR', d, True)
109 lpath = os.path.join(dldir, ud.localfile)
110
111 user = ''
112 if urlo.userinfo:
113 user = urlo.userinfo + '@'
114
115 path = urlo.path
116
117 # Supoprt URIs relative to the user's home directory, with
118 # the tilde syntax. (E.g. <sftp://example.com/~/foo.diff>).
119 if path[:3] == '/~/':
120 path = path[3:]
121
122 remote = '%s%s:%s' % (user, urlo.hostname, path)
123
124 cmd = '%s %s %s %s' % (basecmd, port, commands.mkarg(remote),
125 commands.mkarg(lpath))
126
127 bb.fetch2.check_network_access(d, cmd, uri)
128 runfetchcmd(cmd, d)
129 return True
diff --git a/bitbake/lib/bb/fetch2/ssh.py b/bitbake/lib/bb/fetch2/ssh.py
new file mode 100644
index 0000000000..8b5acbf6db
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/ssh.py
@@ -0,0 +1,127 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3'''
4BitBake 'Fetch' implementations
5
6This implementation is for Secure Shell (SSH), and attempts to comply with the
7IETF secsh internet draft:
8 http://tools.ietf.org/wg/secsh/draft-ietf-secsh-scp-sftp-ssh-uri/
9
10 Currently does not support the sftp parameters, as this uses scp
11 Also does not support the 'fingerprint' connection parameter.
12
13 Please note that '/' is used as host, path separator not ':' as you may
14 be used to, also '~' can be used to specify user HOME, but again after '/'
15
16 Example SRC_URI:
17 SRC_URI = "ssh://user@host.example.com/dir/path/file.txt"
18 SRC_URI = "ssh://user@host.example.com/~/file.txt"
19'''
20
21# Copyright (C) 2006 OpenedHand Ltd.
22#
23#
24# Based in part on svk.py:
25# Copyright (C) 2006 Holger Hans Peter Freyther
26# Based on svn.py:
27# Copyright (C) 2003, 2004 Chris Larson
28# Based on functions from the base bb module:
29# Copyright 2003 Holger Schurig
30#
31#
32# This program is free software; you can redistribute it and/or modify
33# it under the terms of the GNU General Public License version 2 as
34# published by the Free Software Foundation.
35#
36# This program is distributed in the hope that it will be useful,
37# but WITHOUT ANY WARRANTY; without even the implied warranty of
38# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39# GNU General Public License for more details.
40#
41# You should have received a copy of the GNU General Public License along
42# with this program; if not, write to the Free Software Foundation, Inc.,
43# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
44
45import re, os
46from bb import data
47from bb.fetch2 import FetchMethod
48from bb.fetch2 import FetchError
49from bb.fetch2 import logger
50from bb.fetch2 import runfetchcmd
51
52
53__pattern__ = re.compile(r'''
54 \s* # Skip leading whitespace
55 ssh:// # scheme
56 ( # Optional username/password block
57 (?P<user>\S+) # username
58 (:(?P<pass>\S+))? # colon followed by the password (optional)
59 )?
60 (?P<cparam>(;[^;]+)*)? # connection parameters block (optional)
61 @
62 (?P<host>\S+?) # non-greedy match of the host
63 (:(?P<port>[0-9]+))? # colon followed by the port (optional)
64 /
65 (?P<path>[^;]+) # path on the remote system, may be absolute or relative,
66 # and may include the use of '~' to reference the remote home
67 # directory
68 (?P<sparam>(;[^;]+)*)? # parameters block (optional)
69 $
70''', re.VERBOSE)
71
72class SSH(FetchMethod):
73 '''Class to fetch a module or modules via Secure Shell'''
74
75 def supports(self, url, urldata, d):
76 return __pattern__.match(url) != None
77
78 def supports_checksum(self, urldata):
79 return False
80
81 def urldata_init(self, urldata, d):
82 if 'protocol' in urldata.parm and urldata.parm['protocol'] == 'git':
83 raise bb.fetch2.ParameterError(
84 "Invalid protocol - if you wish to fetch from a git " +
85 "repository using ssh, you need to use " +
86 "git:// prefix with protocol=ssh", urldata.url)
87 m = __pattern__.match(urldata.url)
88 path = m.group('path')
89 host = m.group('host')
90 urldata.localpath = os.path.join(d.getVar('DL_DIR', True), os.path.basename(path))
91
92 def download(self, url, urldata, d):
93 dldir = d.getVar('DL_DIR', True)
94
95 m = __pattern__.match(url)
96 path = m.group('path')
97 host = m.group('host')
98 port = m.group('port')
99 user = m.group('user')
100 password = m.group('pass')
101
102 if port:
103 portarg = '-P %s' % port
104 else:
105 portarg = ''
106
107 if user:
108 fr = user
109 if password:
110 fr += ':%s' % password
111 fr += '@%s' % host
112 else:
113 fr = host
114 fr += ':%s' % path
115
116
117 import commands
118 cmd = 'scp -B -r %s %s %s/' % (
119 portarg,
120 commands.mkarg(fr),
121 commands.mkarg(dldir)
122 )
123
124 bb.fetch2.check_network_access(d, cmd, urldata.url)
125
126 runfetchcmd(cmd, d)
127
diff --git a/bitbake/lib/bb/fetch2/svk.py b/bitbake/lib/bb/fetch2/svk.py
new file mode 100644
index 0000000000..ee3823f845
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/svk.py
@@ -0,0 +1,97 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementations
5
6This implementation is for svk. It is based on the svn implementation
7
8"""
9
10# Copyright (C) 2006 Holger Hans Peter Freyther
11# Copyright (C) 2003, 2004 Chris Larson
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26# Based on functions from the base bb module, Copyright 2003 Holger Schurig
27
28import os
29import logging
30import bb
31from bb import data
32from bb.fetch2 import FetchMethod
33from bb.fetch2 import FetchError
34from bb.fetch2 import MissingParameterError
35from bb.fetch2 import logger
36from bb.fetch2 import runfetchcmd
37
38class Svk(FetchMethod):
39 """Class to fetch a module or modules from svk repositories"""
40 def supports(self, url, ud, d):
41 """
42 Check to see if a given url can be fetched with svk.
43 """
44 return ud.type in ['svk']
45
46 def urldata_init(self, ud, d):
47
48 if not "module" in ud.parm:
49 raise MissingParameterError('module', ud.url)
50 else:
51 ud.module = ud.parm["module"]
52
53 ud.revision = ud.parm.get('rev', "")
54
55 ud.localfile = data.expand('%s_%s_%s_%s_%s.tar.gz' % (ud.module.replace('/', '.'), ud.host, ud.path.replace('/', '.'), ud.revision, ud.date), d)
56
57 def need_update(self, url, ud, d):
58 if ud.date == "now":
59 return True
60 if not os.path.exists(ud.localpath):
61 return True
62 return False
63
64 def download(self, loc, ud, d):
65 """Fetch urls"""
66
67 svkroot = ud.host + ud.path
68
69 svkcmd = "svk co -r {%s} %s/%s" % (ud.date, svkroot, ud.module)
70
71 if ud.revision:
72 svkcmd = "svk co -r %s %s/%s" % (ud.revision, svkroot, ud.module)
73
74 # create temp directory
75 localdata = data.createCopy(d)
76 data.update_data(localdata)
77 logger.debug(2, "Fetch: creating temporary directory")
78 bb.utils.mkdirhier(data.expand('${WORKDIR}', localdata))
79 data.setVar('TMPBASE', data.expand('${WORKDIR}/oesvk.XXXXXX', localdata), localdata)
80 tmpfile, errors = bb.process.run(data.getVar('MKTEMPDIRCMD', localdata, True) or "false")
81 tmpfile = tmpfile.strip()
82 if not tmpfile:
83 logger.error()
84 raise FetchError("Fetch: unable to create temporary directory.. make sure 'mktemp' is in the PATH.", loc)
85
86 # check out sources there
87 os.chdir(tmpfile)
88 logger.info("Fetch " + loc)
89 logger.debug(1, "Running %s", svkcmd)
90 runfetchcmd(svkcmd, d, cleanup = [tmpfile])
91
92 os.chdir(os.path.join(tmpfile, os.path.dirname(ud.module)))
93 # tar them up to a defined filename
94 runfetchcmd("tar -czf %s %s" % (ud.localpath, os.path.basename(ud.module)), d, cleanup = [ud.localpath])
95
96 # cleanup
97 bb.utils.prunedir(tmpfile)
diff --git a/bitbake/lib/bb/fetch2/svn.py b/bitbake/lib/bb/fetch2/svn.py
new file mode 100644
index 0000000000..9a779d2448
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/svn.py
@@ -0,0 +1,189 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementation for svn.
5
6"""
7
8# Copyright (C) 2003, 2004 Chris Larson
9# Copyright (C) 2004 Marcin Juszkiewicz
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License version 2 as
13# published by the Free Software Foundation.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License along
21# with this program; if not, write to the Free Software Foundation, Inc.,
22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23#
24# Based on functions from the base bb module, Copyright 2003 Holger Schurig
25
26import os
27import sys
28import logging
29import bb
30from bb import data
31from bb.fetch2 import FetchMethod
32from bb.fetch2 import FetchError
33from bb.fetch2 import MissingParameterError
34from bb.fetch2 import runfetchcmd
35from bb.fetch2 import logger
36
37class Svn(FetchMethod):
38 """Class to fetch a module or modules from svn repositories"""
39 def supports(self, url, ud, d):
40 """
41 Check to see if a given url can be fetched with svn.
42 """
43 return ud.type in ['svn']
44
45 def urldata_init(self, ud, d):
46 """
47 init svn specific variable within url data
48 """
49 if not "module" in ud.parm:
50 raise MissingParameterError('module', ud.url)
51
52 ud.basecmd = d.getVar('FETCHCMD_svn', True)
53
54 ud.module = ud.parm["module"]
55
56 # Create paths to svn checkouts
57 relpath = self._strip_leading_slashes(ud.path)
58 ud.pkgdir = os.path.join(data.expand('${SVNDIR}', d), ud.host, relpath)
59 ud.moddir = os.path.join(ud.pkgdir, ud.module)
60
61 ud.setup_revisons(d)
62
63 if 'rev' in ud.parm:
64 ud.revision = ud.parm['rev']
65
66 ud.localfile = data.expand('%s_%s_%s_%s_.tar.gz' % (ud.module.replace('/', '.'), ud.host, ud.path.replace('/', '.'), ud.revision), d)
67
68 def _buildsvncommand(self, ud, d, command):
69 """
70 Build up an svn commandline based on ud
71 command is "fetch", "update", "info"
72 """
73
74 proto = ud.parm.get('protocol', 'svn')
75
76 svn_rsh = None
77 if proto == "svn+ssh" and "rsh" in ud.parm:
78 svn_rsh = ud.parm["rsh"]
79
80 svnroot = ud.host + ud.path
81
82 options = []
83
84 options.append("--no-auth-cache")
85
86 if ud.user:
87 options.append("--username %s" % ud.user)
88
89 if ud.pswd:
90 options.append("--password %s" % ud.pswd)
91
92 if command == "info":
93 svncmd = "%s info %s %s://%s/%s/" % (ud.basecmd, " ".join(options), proto, svnroot, ud.module)
94 else:
95 suffix = ""
96 if ud.revision:
97 options.append("-r %s" % ud.revision)
98 suffix = "@%s" % (ud.revision)
99
100 if command == "fetch":
101 svncmd = "%s co %s %s://%s/%s%s %s" % (ud.basecmd, " ".join(options), proto, svnroot, ud.module, suffix, ud.module)
102 elif command == "update":
103 svncmd = "%s update %s" % (ud.basecmd, " ".join(options))
104 else:
105 raise FetchError("Invalid svn command %s" % command, ud.url)
106
107 if svn_rsh:
108 svncmd = "svn_RSH=\"%s\" %s" % (svn_rsh, svncmd)
109
110 return svncmd
111
112 def download(self, loc, ud, d):
113 """Fetch url"""
114
115 logger.debug(2, "Fetch: checking for module directory '" + ud.moddir + "'")
116
117 if os.access(os.path.join(ud.moddir, '.svn'), os.R_OK):
118 svnupdatecmd = self._buildsvncommand(ud, d, "update")
119 logger.info("Update " + loc)
120 # update sources there
121 os.chdir(ud.moddir)
122 # We need to attempt to run svn upgrade first in case its an older working format
123 try:
124 runfetchcmd(ud.basecmd + " upgrade", d)
125 except FetchError:
126 pass
127 logger.debug(1, "Running %s", svnupdatecmd)
128 bb.fetch2.check_network_access(d, svnupdatecmd, ud.url)
129 runfetchcmd(svnupdatecmd, d)
130 else:
131 svnfetchcmd = self._buildsvncommand(ud, d, "fetch")
132 logger.info("Fetch " + loc)
133 # check out sources there
134 bb.utils.mkdirhier(ud.pkgdir)
135 os.chdir(ud.pkgdir)
136 logger.debug(1, "Running %s", svnfetchcmd)
137 bb.fetch2.check_network_access(d, svnfetchcmd, ud.url)
138 runfetchcmd(svnfetchcmd, d)
139
140 scmdata = ud.parm.get("scmdata", "")
141 if scmdata == "keep":
142 tar_flags = ""
143 else:
144 tar_flags = "--exclude '.svn'"
145
146 os.chdir(ud.pkgdir)
147 # tar them up to a defined filename
148 runfetchcmd("tar %s -czf %s %s" % (tar_flags, ud.localpath, ud.module), d, cleanup = [ud.localpath])
149
150 def clean(self, ud, d):
151 """ Clean SVN specific files and dirs """
152
153 bb.utils.remove(ud.localpath)
154 bb.utils.remove(ud.moddir, True)
155
156
157 def supports_srcrev(self):
158 return True
159
160 def _revision_key(self, url, ud, d, name):
161 """
162 Return a unique key for the url
163 """
164 return "svn:" + ud.moddir
165
166 def _latest_revision(self, url, ud, d, name):
167 """
168 Return the latest upstream revision number
169 """
170 bb.fetch2.check_network_access(d, self._buildsvncommand(ud, d, "info"))
171
172 output = runfetchcmd("LANG=C LC_ALL=C " + self._buildsvncommand(ud, d, "info"), d, True)
173
174 revision = None
175 for line in output.splitlines():
176 if "Last Changed Rev" in line:
177 revision = line.split(":")[1].strip()
178
179 return revision
180
181 def sortable_revision(self, url, ud, d, name):
182 """
183 Return a sortable revision number which in our case is the revision number
184 """
185
186 return False, self._build_revision(url, ud, d)
187
188 def _build_revision(self, url, ud, d):
189 return ud.revision
diff --git a/bitbake/lib/bb/fetch2/wget.py b/bitbake/lib/bb/fetch2/wget.py
new file mode 100644
index 0000000000..131016ce89
--- /dev/null
+++ b/bitbake/lib/bb/fetch2/wget.py
@@ -0,0 +1,97 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'Fetch' implementations
5
6Classes for obtaining upstream sources for the
7BitBake build tools.
8
9"""
10
11# Copyright (C) 2003, 2004 Chris Larson
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25#
26# Based on functions from the base bb module, Copyright 2003 Holger Schurig
27
28import os
29import logging
30import bb
31import urllib
32from bb import data
33from bb.fetch2 import FetchMethod
34from bb.fetch2 import FetchError
35from bb.fetch2 import logger
36from bb.fetch2 import runfetchcmd
37
38class Wget(FetchMethod):
39 """Class to fetch urls via 'wget'"""
40 def supports(self, url, ud, d):
41 """
42 Check to see if a given url can be fetched with wget.
43 """
44 return ud.type in ['http', 'https', 'ftp']
45
46 def recommends_checksum(self, urldata):
47 return True
48
49 def urldata_init(self, ud, d):
50 if 'protocol' in ud.parm:
51 if ud.parm['protocol'] == 'git':
52 raise bb.fetch2.ParameterError("Invalid protocol - if you wish to fetch from a git repository using http, you need to instead use the git:// prefix with protocol=http", ud.url)
53
54 if 'downloadfilename' in ud.parm:
55 ud.basename = ud.parm['downloadfilename']
56 else:
57 ud.basename = os.path.basename(ud.path)
58
59 ud.localfile = data.expand(urllib.unquote(ud.basename), d)
60
61 def download(self, uri, ud, d, checkonly = False):
62 """Fetch urls"""
63
64 basecmd = d.getVar("FETCHCMD_wget", True) or "/usr/bin/env wget -t 2 -T 30 -nv --passive-ftp --no-check-certificate"
65
66 if not checkonly and 'downloadfilename' in ud.parm:
67 dldir = d.getVar("DL_DIR", True)
68 bb.utils.mkdirhier(os.path.dirname(dldir + os.sep + ud.localfile))
69 basecmd += " -O " + dldir + os.sep + ud.localfile
70
71 if checkonly:
72 fetchcmd = d.getVar("CHECKCOMMAND_wget", True) or d.expand(basecmd + " --spider '${URI}'")
73 elif os.path.exists(ud.localpath):
74 # file exists, but we didnt complete it.. trying again..
75 fetchcmd = d.getVar("RESUMECOMMAND_wget", True) or d.expand(basecmd + " -c -P ${DL_DIR} '${URI}'")
76 else:
77 fetchcmd = d.getVar("FETCHCOMMAND_wget", True) or d.expand(basecmd + " -P ${DL_DIR} '${URI}'")
78
79 uri = uri.split(";")[0]
80
81 fetchcmd = fetchcmd.replace("${URI}", uri.split(";")[0])
82 fetchcmd = fetchcmd.replace("${FILE}", ud.basename)
83 if not checkonly:
84 logger.info("fetch " + uri)
85 logger.debug(2, "executing " + fetchcmd)
86 bb.fetch2.check_network_access(d, fetchcmd)
87 runfetchcmd(fetchcmd, d, quiet=checkonly)
88
89 # Sanity check since wget can pretend it succeed when it didn't
90 # Also, this used to happen if sourceforge sent us to the mirror page
91 if not os.path.exists(ud.localpath) and not checkonly:
92 raise FetchError("The fetch command returned success for url %s but %s doesn't exist?!" % (uri, ud.localpath), uri)
93
94 return True
95
96 def checkstatus(self, uri, ud, d):
97 return self.download(uri, ud, d, True)
diff --git a/bitbake/lib/bb/methodpool.py b/bitbake/lib/bb/methodpool.py
new file mode 100644
index 0000000000..bf2e9f5542
--- /dev/null
+++ b/bitbake/lib/bb/methodpool.py
@@ -0,0 +1,29 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4#
5# Copyright (C) 2006 Holger Hans Peter Freyther
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20from bb.utils import better_compile, better_exec
21
22def insert_method(modulename, code, fn):
23 """
24 Add code of a module should be added. The methods
25 will be simply added, no checking will be done
26 """
27 comp = better_compile(code, modulename, fn )
28 better_exec(comp, None, code, fn)
29
diff --git a/bitbake/lib/bb/monitordisk.py b/bitbake/lib/bb/monitordisk.py
new file mode 100644
index 0000000000..3e6ecbd4e2
--- /dev/null
+++ b/bitbake/lib/bb/monitordisk.py
@@ -0,0 +1,265 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# Copyright (C) 2012 Robert Yang
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20import os, logging, re, sys
21import bb
22logger = logging.getLogger("BitBake.Monitor")
23
24def printErr(info):
25 logger.error("%s\n Disk space monitor will NOT be enabled" % info)
26
27def convertGMK(unit):
28
29 """ Convert the space unit G, M, K, the unit is case-insensitive """
30
31 unitG = re.match('([1-9][0-9]*)[gG]\s?$', unit)
32 if unitG:
33 return int(unitG.group(1)) * (1024 ** 3)
34 unitM = re.match('([1-9][0-9]*)[mM]\s?$', unit)
35 if unitM:
36 return int(unitM.group(1)) * (1024 ** 2)
37 unitK = re.match('([1-9][0-9]*)[kK]\s?$', unit)
38 if unitK:
39 return int(unitK.group(1)) * 1024
40 unitN = re.match('([1-9][0-9]*)\s?$', unit)
41 if unitN:
42 return int(unitN.group(1))
43 else:
44 return None
45
46def getMountedDev(path):
47
48 """ Get the device mounted at the path, uses /proc/mounts """
49
50 # Get the mount point of the filesystem containing path
51 # st_dev is the ID of device containing file
52 parentDev = os.stat(path).st_dev
53 currentDev = parentDev
54 # When the current directory's device is different from the
55 # parrent's, then the current directory is a mount point
56 while parentDev == currentDev:
57 mountPoint = path
58 # Use dirname to get the parrent's directory
59 path = os.path.dirname(path)
60 # Reach the "/"
61 if path == mountPoint:
62 break
63 parentDev= os.stat(path).st_dev
64
65 try:
66 with open("/proc/mounts", "r") as ifp:
67 for line in ifp:
68 procLines = line.rstrip('\n').split()
69 if procLines[1] == mountPoint:
70 return procLines[0]
71 except EnvironmentError:
72 pass
73 return None
74
75def getDiskData(BBDirs, configuration):
76
77 """Prepare disk data for disk space monitor"""
78
79 # Save the device IDs, need the ID to be unique (the dictionary's key is
80 # unique), so that when more than one directories are located in the same
81 # device, we just monitor it once
82 devDict = {}
83 for pathSpaceInode in BBDirs.split():
84 # The input format is: "dir,space,inode", dir is a must, space
85 # and inode are optional
86 pathSpaceInodeRe = re.match('([^,]*),([^,]*),([^,]*),?(.*)', pathSpaceInode)
87 if not pathSpaceInodeRe:
88 printErr("Invalid value in BB_DISKMON_DIRS: %s" % pathSpaceInode)
89 return None
90
91 action = pathSpaceInodeRe.group(1)
92 if action not in ("ABORT", "STOPTASKS", "WARN"):
93 printErr("Unknown disk space monitor action: %s" % action)
94 return None
95
96 path = os.path.realpath(pathSpaceInodeRe.group(2))
97 if not path:
98 printErr("Invalid path value in BB_DISKMON_DIRS: %s" % pathSpaceInode)
99 return None
100
101 # The disk space or inode is optional, but it should have a correct
102 # value once it is specified
103 minSpace = pathSpaceInodeRe.group(3)
104 if minSpace:
105 minSpace = convertGMK(minSpace)
106 if not minSpace:
107 printErr("Invalid disk space value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(3))
108 return None
109 else:
110 # None means that it is not specified
111 minSpace = None
112
113 minInode = pathSpaceInodeRe.group(4)
114 if minInode:
115 minInode = convertGMK(minInode)
116 if not minInode:
117 printErr("Invalid inode value in BB_DISKMON_DIRS: %s" % pathSpaceInodeRe.group(4))
118 return None
119 else:
120 # None means that it is not specified
121 minInode = None
122
123 if minSpace is None and minInode is None:
124 printErr("No disk space or inode value in found BB_DISKMON_DIRS: %s" % pathSpaceInode)
125 return None
126 # mkdir for the directory since it may not exist, for example the
127 # DL_DIR may not exist at the very beginning
128 if not os.path.exists(path):
129 bb.utils.mkdirhier(path)
130 dev = getMountedDev(path)
131 # Use path/action as the key
132 devDict[os.path.join(path, action)] = [dev, minSpace, minInode]
133
134 return devDict
135
136def getInterval(configuration):
137
138 """ Get the disk space interval """
139
140 # The default value is 50M and 5K.
141 spaceDefault = 50 * 1024 * 1024
142 inodeDefault = 5 * 1024
143
144 interval = configuration.getVar("BB_DISKMON_WARNINTERVAL", True)
145 if not interval:
146 return spaceDefault, inodeDefault
147 else:
148 # The disk space or inode interval is optional, but it should
149 # have a correct value once it is specified
150 intervalRe = re.match('([^,]*),?\s*(.*)', interval)
151 if intervalRe:
152 intervalSpace = intervalRe.group(1)
153 if intervalSpace:
154 intervalSpace = convertGMK(intervalSpace)
155 if not intervalSpace:
156 printErr("Invalid disk space interval value in BB_DISKMON_WARNINTERVAL: %s" % intervalRe.group(1))
157 return None, None
158 else:
159 intervalSpace = spaceDefault
160 intervalInode = intervalRe.group(2)
161 if intervalInode:
162 intervalInode = convertGMK(intervalInode)
163 if not intervalInode:
164 printErr("Invalid disk inode interval value in BB_DISKMON_WARNINTERVAL: %s" % intervalRe.group(2))
165 return None, None
166 else:
167 intervalInode = inodeDefault
168 return intervalSpace, intervalInode
169 else:
170 printErr("Invalid interval value in BB_DISKMON_WARNINTERVAL: %s" % interval)
171 return None, None
172
173class diskMonitor:
174
175 """Prepare the disk space monitor data"""
176
177 def __init__(self, configuration):
178
179 self.enableMonitor = False
180 self.configuration = configuration
181
182 BBDirs = configuration.getVar("BB_DISKMON_DIRS", True) or None
183 if BBDirs:
184 self.devDict = getDiskData(BBDirs, configuration)
185 if self.devDict:
186 self.spaceInterval, self.inodeInterval = getInterval(configuration)
187 if self.spaceInterval and self.inodeInterval:
188 self.enableMonitor = True
189 # These are for saving the previous disk free space and inode, we
190 # use them to avoid print too many warning messages
191 self.preFreeS = {}
192 self.preFreeI = {}
193 # This is for STOPTASKS and ABORT, to avoid print the message repeatly
194 # during waiting the tasks to finish
195 self.checked = {}
196 for k in self.devDict:
197 self.preFreeS[k] = 0
198 self.preFreeI[k] = 0
199 self.checked[k] = False
200 if self.spaceInterval is None and self.inodeInterval is None:
201 self.enableMonitor = False
202
203 def check(self, rq):
204
205 """ Take action for the monitor """
206
207 if self.enableMonitor:
208 for k in self.devDict:
209 path = os.path.dirname(k)
210 action = os.path.basename(k)
211 dev = self.devDict[k][0]
212 minSpace = self.devDict[k][1]
213 minInode = self.devDict[k][2]
214
215 st = os.statvfs(path)
216
217 # The free space, float point number
218 freeSpace = st.f_bavail * st.f_frsize
219
220 if minSpace and freeSpace < minSpace:
221 # Always show warning, the self.checked would always be False if the action is WARN
222 if self.preFreeS[k] == 0 or self.preFreeS[k] - freeSpace > self.spaceInterval and not self.checked[k]:
223 logger.warn("The free space of %s (%s) is running low (%.3fGB left)" % \
224 (path, dev, freeSpace / 1024 / 1024 / 1024.0))
225 self.preFreeS[k] = freeSpace
226
227 if action == "STOPTASKS" and not self.checked[k]:
228 logger.error("No new tasks can be executed since the disk space monitor action is \"STOPTASKS\"!")
229 self.checked[k] = True
230 rq.finish_runqueue(False)
231 bb.event.fire(bb.event.DiskFull(dev, 'disk', freeSpace, path), self.configuration)
232 elif action == "ABORT" and not self.checked[k]:
233 logger.error("Immediately abort since the disk space monitor action is \"ABORT\"!")
234 self.checked[k] = True
235 rq.finish_runqueue(True)
236 bb.event.fire(bb.event.DiskFull(dev, 'disk', freeSpace, path), self.configuration)
237
238 # The free inodes, float point number
239 freeInode = st.f_favail
240
241 if minInode and freeInode < minInode:
242 # Some fs formats' (e.g., btrfs) statvfs.f_files (inodes) is
243 # zero, this is a feature of the fs, we disable the inode
244 # checking for such a fs.
245 if st.f_files == 0:
246 logger.warn("Inode check for %s is unavaliable, will remove it from disk monitor" % path)
247 self.devDict[k][2] = None
248 continue
249 # Always show warning, the self.checked would always be False if the action is WARN
250 if self.preFreeI[k] == 0 or self.preFreeI[k] - freeInode > self.inodeInterval and not self.checked[k]:
251 logger.warn("The free inode of %s (%s) is running low (%.3fK left)" % \
252 (path, dev, freeInode / 1024.0))
253 self.preFreeI[k] = freeInode
254
255 if action == "STOPTASKS" and not self.checked[k]:
256 logger.error("No new tasks can be executed since the disk space monitor action is \"STOPTASKS\"!")
257 self.checked[k] = True
258 rq.finish_runqueue(False)
259 bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration)
260 elif action == "ABORT" and not self.checked[k]:
261 logger.error("Immediately abort since the disk space monitor action is \"ABORT\"!")
262 self.checked[k] = True
263 rq.finish_runqueue(True)
264 bb.event.fire(bb.event.DiskFull(dev, 'inode', freeInode, path), self.configuration)
265 return
diff --git a/bitbake/lib/bb/msg.py b/bitbake/lib/bb/msg.py
new file mode 100644
index 0000000000..59769707e0
--- /dev/null
+++ b/bitbake/lib/bb/msg.py
@@ -0,0 +1,182 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake 'msg' implementation
5
6Message handling infrastructure for bitbake
7
8"""
9
10# Copyright (C) 2006 Richard Purdie
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License version 2 as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along
22# with this program; if not, write to the Free Software Foundation, Inc.,
23# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24
25import sys
26import copy
27import logging
28import collections
29from itertools import groupby
30import warnings
31import bb
32import bb.event
33
34class BBLogFormatter(logging.Formatter):
35 """Formatter which ensures that our 'plain' messages (logging.INFO + 1) are used as is"""
36
37 DEBUG3 = logging.DEBUG - 2
38 DEBUG2 = logging.DEBUG - 1
39 DEBUG = logging.DEBUG
40 VERBOSE = logging.INFO - 1
41 NOTE = logging.INFO
42 PLAIN = logging.INFO + 1
43 ERROR = logging.ERROR
44 WARNING = logging.WARNING
45 CRITICAL = logging.CRITICAL
46
47 levelnames = {
48 DEBUG3 : 'DEBUG',
49 DEBUG2 : 'DEBUG',
50 DEBUG : 'DEBUG',
51 VERBOSE: 'NOTE',
52 NOTE : 'NOTE',
53 PLAIN : '',
54 WARNING : 'WARNING',
55 ERROR : 'ERROR',
56 CRITICAL: 'ERROR',
57 }
58
59 color_enabled = False
60 BASECOLOR, BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(29,38)
61
62 COLORS = {
63 DEBUG3 : CYAN,
64 DEBUG2 : CYAN,
65 DEBUG : CYAN,
66 VERBOSE : BASECOLOR,
67 NOTE : BASECOLOR,
68 PLAIN : BASECOLOR,
69 WARNING : YELLOW,
70 ERROR : RED,
71 CRITICAL: RED,
72 }
73
74 BLD = '\033[1;%dm'
75 STD = '\033[%dm'
76 RST = '\033[0m'
77
78 def getLevelName(self, levelno):
79 try:
80 return self.levelnames[levelno]
81 except KeyError:
82 self.levelnames[levelno] = value = 'Level %d' % levelno
83 return value
84
85 def format(self, record):
86 record.levelname = self.getLevelName(record.levelno)
87 if record.levelno == self.PLAIN:
88 msg = record.getMessage()
89 else:
90 if self.color_enabled:
91 record = self.colorize(record)
92 msg = logging.Formatter.format(self, record)
93
94 if hasattr(record, 'bb_exc_info'):
95 etype, value, tb = record.bb_exc_info
96 formatted = bb.exceptions.format_exception(etype, value, tb, limit=5)
97 msg += '\n' + ''.join(formatted)
98 return msg
99
100 def colorize(self, record):
101 color = self.COLORS[record.levelno]
102 if self.color_enabled and color is not None:
103 record = copy.copy(record)
104 record.levelname = "".join([self.BLD % color, record.levelname, self.RST])
105 record.msg = "".join([self.STD % color, record.msg, self.RST])
106 return record
107
108 def enable_color(self):
109 self.color_enabled = True
110
111class BBLogFilter(object):
112 def __init__(self, handler, level, debug_domains):
113 self.stdlevel = level
114 self.debug_domains = debug_domains
115 loglevel = level
116 for domain in debug_domains:
117 if debug_domains[domain] < loglevel:
118 loglevel = debug_domains[domain]
119 handler.setLevel(loglevel)
120 handler.addFilter(self)
121
122 def filter(self, record):
123 if record.levelno >= self.stdlevel:
124 return True
125 if record.name in self.debug_domains and record.levelno >= self.debug_domains[record.name]:
126 return True
127 return False
128
129
130
131# Message control functions
132#
133
134loggerDefaultDebugLevel = 0
135loggerDefaultVerbose = False
136loggerVerboseLogs = False
137loggerDefaultDomains = []
138
139def init_msgconfig(verbose, debug, debug_domains = []):
140 """
141 Set default verbosity and debug levels config the logger
142 """
143 bb.msg.loggerDefaultDebugLevel = debug
144 bb.msg.loggerDefaultVerbose = verbose
145 if verbose:
146 bb.msg.loggerVerboseLogs = True
147 bb.msg.loggerDefaultDomains = debug_domains
148
149def constructLogOptions():
150 debug = loggerDefaultDebugLevel
151 verbose = loggerDefaultVerbose
152 domains = loggerDefaultDomains
153
154 if debug:
155 level = BBLogFormatter.DEBUG - debug + 1
156 elif verbose:
157 level = BBLogFormatter.VERBOSE
158 else:
159 level = BBLogFormatter.NOTE
160
161 debug_domains = {}
162 for (domainarg, iterator) in groupby(domains):
163 dlevel = len(tuple(iterator))
164 debug_domains["BitBake.%s" % domainarg] = logging.DEBUG - dlevel + 1
165 return level, debug_domains
166
167def addDefaultlogFilter(handler):
168 level, debug_domains = constructLogOptions()
169
170 BBLogFilter(handler, level, debug_domains)
171
172#
173# Message handling functions
174#
175
176def fatal(msgdomain, msg):
177 if msgdomain:
178 logger = logging.getLogger("BitBake.%s" % msgdomain)
179 else:
180 logger = logging.getLogger("BitBake")
181 logger.critical(msg)
182 sys.exit(1)
diff --git a/bitbake/lib/bb/namedtuple_with_abc.py b/bitbake/lib/bb/namedtuple_with_abc.py
new file mode 100644
index 0000000000..f5e0a3f3d5
--- /dev/null
+++ b/bitbake/lib/bb/namedtuple_with_abc.py
@@ -0,0 +1,255 @@
1# http://code.activestate.com/recipes/577629-namedtupleabc-abstract-base-class-mix-in-for-named/
2#!/usr/bin/env python
3# Copyright (c) 2011 Jan Kaliszewski (zuo). Available under the MIT License.
4
5"""
6namedtuple_with_abc.py:
7* named tuple mix-in + ABC (abstract base class) recipe,
8* works under Python 2.6, 2.7 as well as 3.x.
9
10Import this module to patch collections.namedtuple() factory function
11-- enriching it with the 'abc' attribute (an abstract base class + mix-in
12for named tuples) and decorating it with a wrapper that registers each
13newly created named tuple as a subclass of namedtuple.abc.
14
15How to import:
16 import collections, namedtuple_with_abc
17or:
18 import namedtuple_with_abc
19 from collections import namedtuple
20 # ^ in this variant you must import namedtuple function
21 # *after* importing namedtuple_with_abc module
22or simply:
23 from namedtuple_with_abc import namedtuple
24
25Simple usage example:
26 class Credentials(namedtuple.abc):
27 _fields = 'username password'
28 def __str__(self):
29 return ('{0.__class__.__name__}'
30 '(username={0.username}, password=...)'.format(self))
31 print(Credentials("alice", "Alice's password"))
32
33For more advanced examples -- see below the "if __name__ == '__main__':".
34"""
35
36import collections
37from abc import ABCMeta, abstractproperty
38from functools import wraps
39from sys import version_info
40
41__all__ = ('namedtuple',)
42_namedtuple = collections.namedtuple
43
44
45class _NamedTupleABCMeta(ABCMeta):
46 '''The metaclass for the abstract base class + mix-in for named tuples.'''
47 def __new__(mcls, name, bases, namespace):
48 fields = namespace.get('_fields')
49 for base in bases:
50 if fields is not None:
51 break
52 fields = getattr(base, '_fields', None)
53 if not isinstance(fields, abstractproperty):
54 basetuple = _namedtuple(name, fields)
55 bases = (basetuple,) + bases
56 namespace.pop('_fields', None)
57 namespace.setdefault('__doc__', basetuple.__doc__)
58 namespace.setdefault('__slots__', ())
59 return ABCMeta.__new__(mcls, name, bases, namespace)
60
61
62exec(
63 # Python 2.x metaclass declaration syntax
64 """class _NamedTupleABC(object):
65 '''The abstract base class + mix-in for named tuples.'''
66 __metaclass__ = _NamedTupleABCMeta
67 _fields = abstractproperty()""" if version_info[0] < 3 else
68 # Python 3.x metaclass declaration syntax
69 """class _NamedTupleABC(metaclass=_NamedTupleABCMeta):
70 '''The abstract base class + mix-in for named tuples.'''
71 _fields = abstractproperty()"""
72)
73
74
75_namedtuple.abc = _NamedTupleABC
76#_NamedTupleABC.register(type(version_info)) # (and similar, in the future...)
77
78@wraps(_namedtuple)
79def namedtuple(*args, **kwargs):
80 '''Named tuple factory with namedtuple.abc subclass registration.'''
81 cls = _namedtuple(*args, **kwargs)
82 _NamedTupleABC.register(cls)
83 return cls
84
85collections.namedtuple = namedtuple
86
87
88
89
90if __name__ == '__main__':
91
92 '''Examples and explanations'''
93
94 # Simple usage
95
96 class MyRecord(namedtuple.abc):
97 _fields = 'x y z' # such form will be transformed into ('x', 'y', 'z')
98 def _my_custom_method(self):
99 return list(self._asdict().items())
100 # (the '_fields' attribute belongs to the named tuple public API anyway)
101
102 rec = MyRecord(1, 2, 3)
103 print(rec)
104 print(rec._my_custom_method())
105 print(rec._replace(y=222))
106 print(rec._replace(y=222)._my_custom_method())
107
108 # Custom abstract classes...
109
110 class MyAbstractRecord(namedtuple.abc):
111 def _my_custom_method(self):
112 return list(self._asdict().items())
113
114 try:
115 MyAbstractRecord() # (abstract classes cannot be instantiated)
116 except TypeError as exc:
117 print(exc)
118
119 class AnotherAbstractRecord(MyAbstractRecord):
120 def __str__(self):
121 return '<<<{0}>>>'.format(super(AnotherAbstractRecord,
122 self).__str__())
123
124 # ...and their non-abstract subclasses
125
126 class MyRecord2(MyAbstractRecord):
127 _fields = 'a, b'
128
129 class MyRecord3(AnotherAbstractRecord):
130 _fields = 'p', 'q', 'r'
131
132 rec2 = MyRecord2('foo', 'bar')
133 print(rec2)
134 print(rec2._my_custom_method())
135 print(rec2._replace(b=222))
136 print(rec2._replace(b=222)._my_custom_method())
137
138 rec3 = MyRecord3('foo', 'bar', 'baz')
139 print(rec3)
140 print(rec3._my_custom_method())
141 print(rec3._replace(q=222))
142 print(rec3._replace(q=222)._my_custom_method())
143
144 # You can also subclass non-abstract ones...
145
146 class MyRecord33(MyRecord3):
147 def __str__(self):
148 return '< {0!r}, ..., {0!r} >'.format(self.p, self.r)
149
150 rec33 = MyRecord33('foo', 'bar', 'baz')
151 print(rec33)
152 print(rec33._my_custom_method())
153 print(rec33._replace(q=222))
154 print(rec33._replace(q=222)._my_custom_method())
155
156 # ...and even override the magic '_fields' attribute again
157
158 class MyRecord345(MyRecord3):
159 _fields = 'e f g h i j k'
160
161 rec345 = MyRecord345(1, 2, 3, 4, 3, 2, 1)
162 print(rec345)
163 print(rec345._my_custom_method())
164 print(rec345._replace(f=222))
165 print(rec345._replace(f=222)._my_custom_method())
166
167 # Mixing-in some other classes is also possible:
168
169 class MyMixIn(object):
170 def method(self):
171 return "MyMixIn.method() called"
172 def _my_custom_method(self):
173 return "MyMixIn._my_custom_method() called"
174 def count(self, item):
175 return "MyMixIn.count({0}) called".format(item)
176 def _asdict(self): # (cannot override a namedtuple method, see below)
177 return "MyMixIn._asdict() called"
178
179 class MyRecord4(MyRecord33, MyMixIn): # mix-in on the right
180 _fields = 'j k l x'
181
182 class MyRecord5(MyMixIn, MyRecord33): # mix-in on the left
183 _fields = 'j k l x y'
184
185 rec4 = MyRecord4(1, 2, 3, 2)
186 print(rec4)
187 print(rec4.method())
188 print(rec4._my_custom_method()) # MyRecord33's
189 print(rec4.count(2)) # tuple's
190 print(rec4._replace(k=222))
191 print(rec4._replace(k=222).method())
192 print(rec4._replace(k=222)._my_custom_method()) # MyRecord33's
193 print(rec4._replace(k=222).count(8)) # tuple's
194
195 rec5 = MyRecord5(1, 2, 3, 2, 1)
196 print(rec5)
197 print(rec5.method())
198 print(rec5._my_custom_method()) # MyMixIn's
199 print(rec5.count(2)) # MyMixIn's
200 print(rec5._replace(k=222))
201 print(rec5._replace(k=222).method())
202 print(rec5._replace(k=222)._my_custom_method()) # MyMixIn's
203 print(rec5._replace(k=222).count(2)) # MyMixIn's
204
205 # None that behavior: the standard namedtuple methods cannot be
206 # overriden by a foreign mix-in -- even if the mix-in is declared
207 # as the leftmost base class (but, obviously, you can override them
208 # in the defined class or its subclasses):
209
210 print(rec4._asdict()) # (returns a dict, not "MyMixIn._asdict() called")
211 print(rec5._asdict()) # (returns a dict, not "MyMixIn._asdict() called")
212
213 class MyRecord6(MyRecord33):
214 _fields = 'j k l x y z'
215 def _asdict(self):
216 return "MyRecord6._asdict() called"
217 rec6 = MyRecord6(1, 2, 3, 1, 2, 3)
218 print(rec6._asdict()) # (this returns "MyRecord6._asdict() called")
219
220 # All that record classes are real subclasses of namedtuple.abc:
221
222 assert issubclass(MyRecord, namedtuple.abc)
223 assert issubclass(MyAbstractRecord, namedtuple.abc)
224 assert issubclass(AnotherAbstractRecord, namedtuple.abc)
225 assert issubclass(MyRecord2, namedtuple.abc)
226 assert issubclass(MyRecord3, namedtuple.abc)
227 assert issubclass(MyRecord33, namedtuple.abc)
228 assert issubclass(MyRecord345, namedtuple.abc)
229 assert issubclass(MyRecord4, namedtuple.abc)
230 assert issubclass(MyRecord5, namedtuple.abc)
231 assert issubclass(MyRecord6, namedtuple.abc)
232
233 # ...but abstract ones are not subclasses of tuple
234 # (and this is what you probably want):
235
236 assert not issubclass(MyAbstractRecord, tuple)
237 assert not issubclass(AnotherAbstractRecord, tuple)
238
239 assert issubclass(MyRecord, tuple)
240 assert issubclass(MyRecord2, tuple)
241 assert issubclass(MyRecord3, tuple)
242 assert issubclass(MyRecord33, tuple)
243 assert issubclass(MyRecord345, tuple)
244 assert issubclass(MyRecord4, tuple)
245 assert issubclass(MyRecord5, tuple)
246 assert issubclass(MyRecord6, tuple)
247
248 # Named tuple classes created with namedtuple() factory function
249 # (in the "traditional" way) are registered as "virtual" subclasses
250 # of namedtuple.abc:
251
252 MyTuple = namedtuple('MyTuple', 'a b c')
253 mt = MyTuple(1, 2, 3)
254 assert issubclass(MyTuple, namedtuple.abc)
255 assert isinstance(mt, namedtuple.abc)
diff --git a/bitbake/lib/bb/parse/__init__.py b/bitbake/lib/bb/parse/__init__.py
new file mode 100644
index 0000000000..c973f6fdbf
--- /dev/null
+++ b/bitbake/lib/bb/parse/__init__.py
@@ -0,0 +1,146 @@
1"""
2BitBake Parsers
3
4File parsers for the BitBake build tools.
5
6"""
7
8
9# Copyright (C) 2003, 2004 Chris Larson
10# Copyright (C) 2003, 2004 Phil Blundell
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License version 2 as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along
22# with this program; if not, write to the Free Software Foundation, Inc.,
23# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24#
25# Based on functions from the base bb module, Copyright 2003 Holger Schurig
26
27handlers = []
28
29import os
30import stat
31import logging
32import bb
33import bb.utils
34import bb.siggen
35
36logger = logging.getLogger("BitBake.Parsing")
37
38class ParseError(Exception):
39 """Exception raised when parsing fails"""
40 def __init__(self, msg, filename, lineno=0):
41 self.msg = msg
42 self.filename = filename
43 self.lineno = lineno
44 Exception.__init__(self, msg, filename, lineno)
45
46 def __str__(self):
47 if self.lineno:
48 return "ParseError at %s:%d: %s" % (self.filename, self.lineno, self.msg)
49 else:
50 return "ParseError in %s: %s" % (self.filename, self.msg)
51
52class SkipPackage(Exception):
53 """Exception raised to skip this package"""
54
55__mtime_cache = {}
56def cached_mtime(f):
57 if f not in __mtime_cache:
58 __mtime_cache[f] = os.stat(f)[stat.ST_MTIME]
59 return __mtime_cache[f]
60
61def cached_mtime_noerror(f):
62 if f not in __mtime_cache:
63 try:
64 __mtime_cache[f] = os.stat(f)[stat.ST_MTIME]
65 except OSError:
66 return 0
67 return __mtime_cache[f]
68
69def update_mtime(f):
70 __mtime_cache[f] = os.stat(f)[stat.ST_MTIME]
71 return __mtime_cache[f]
72
73def mark_dependency(d, f):
74 if f.startswith('./'):
75 f = "%s/%s" % (os.getcwd(), f[2:])
76 deps = (d.getVar('__depends') or []) + [(f, cached_mtime(f))]
77 d.setVar('__depends', deps)
78
79def supports(fn, data):
80 """Returns true if we have a handler for this file, false otherwise"""
81 for h in handlers:
82 if h['supports'](fn, data):
83 return 1
84 return 0
85
86def handle(fn, data, include = 0):
87 """Call the handler that is appropriate for this file"""
88 for h in handlers:
89 if h['supports'](fn, data):
90 with data.inchistory.include(fn):
91 return h['handle'](fn, data, include)
92 raise ParseError("not a BitBake file", fn)
93
94def init(fn, data):
95 for h in handlers:
96 if h['supports'](fn):
97 return h['init'](data)
98
99def init_parser(d):
100 bb.parse.siggen = bb.siggen.init(d)
101
102def resolve_file(fn, d):
103 if not os.path.isabs(fn):
104 bbpath = d.getVar("BBPATH", True)
105 newfn = bb.utils.which(bbpath, fn)
106 if not newfn:
107 raise IOError("file %s not found in %s" % (fn, bbpath))
108 fn = newfn
109
110 if not os.path.isfile(fn):
111 raise IOError("file %s not found" % fn)
112
113 logger.debug(2, "LOAD %s", fn)
114 return fn
115
116# Used by OpenEmbedded metadata
117__pkgsplit_cache__={}
118def vars_from_file(mypkg, d):
119 if not mypkg:
120 return (None, None, None)
121 if mypkg in __pkgsplit_cache__:
122 return __pkgsplit_cache__[mypkg]
123
124 myfile = os.path.splitext(os.path.basename(mypkg))
125 parts = myfile[0].split('_')
126 __pkgsplit_cache__[mypkg] = parts
127 if len(parts) > 3:
128 raise ParseError("Unable to generate default variables from filename (too many underscores)", mypkg)
129 exp = 3 - len(parts)
130 tmplist = []
131 while exp != 0:
132 exp -= 1
133 tmplist.append(None)
134 parts.extend(tmplist)
135 return parts
136
137def get_file_depends(d):
138 '''Return the dependent files'''
139 dep_files = []
140 depends = d.getVar('__base_depends', True) or []
141 depends = depends + (d.getVar('__depends', True) or [])
142 for (fn, _) in depends:
143 dep_files.append(os.path.abspath(fn))
144 return " ".join(dep_files)
145
146from bb.parse.parse_py import __version__, ConfHandler, BBHandler
diff --git a/bitbake/lib/bb/parse/ast.py b/bitbake/lib/bb/parse/ast.py
new file mode 100644
index 0000000000..d4b8b09543
--- /dev/null
+++ b/bitbake/lib/bb/parse/ast.py
@@ -0,0 +1,482 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4 AbstractSyntaxTree classes for the Bitbake language
5"""
6
7# Copyright (C) 2003, 2004 Chris Larson
8# Copyright (C) 2003, 2004 Phil Blundell
9# Copyright (C) 2009 Holger Hans Peter Freyther
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License version 2 as
13# published by the Free Software Foundation.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License along
21# with this program; if not, write to the Free Software Foundation, Inc.,
22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
24from __future__ import absolute_import
25from future_builtins import filter
26import re
27import string
28import logging
29import bb
30import itertools
31from bb import methodpool
32from bb.parse import logger
33
34_bbversions_re = re.compile(r"\[(?P<from>[0-9]+)-(?P<to>[0-9]+)\]")
35
36class StatementGroup(list):
37 def eval(self, data):
38 for statement in self:
39 statement.eval(data)
40
41class AstNode(object):
42 def __init__(self, filename, lineno):
43 self.filename = filename
44 self.lineno = lineno
45
46class IncludeNode(AstNode):
47 def __init__(self, filename, lineno, what_file, force):
48 AstNode.__init__(self, filename, lineno)
49 self.what_file = what_file
50 self.force = force
51
52 def eval(self, data):
53 """
54 Include the file and evaluate the statements
55 """
56 s = data.expand(self.what_file)
57 logger.debug(2, "CONF %s:%s: including %s", self.filename, self.lineno, s)
58
59 # TODO: Cache those includes... maybe not here though
60 if self.force:
61 bb.parse.ConfHandler.include(self.filename, s, self.lineno, data, "include required")
62 else:
63 bb.parse.ConfHandler.include(self.filename, s, self.lineno, data, False)
64
65class ExportNode(AstNode):
66 def __init__(self, filename, lineno, var):
67 AstNode.__init__(self, filename, lineno)
68 self.var = var
69
70 def eval(self, data):
71 data.setVarFlag(self.var, "export", 1, op = 'exported')
72
73class DataNode(AstNode):
74 """
75 Various data related updates. For the sake of sanity
76 we have one class doing all this. This means that all
77 this need to be re-evaluated... we might be able to do
78 that faster with multiple classes.
79 """
80 def __init__(self, filename, lineno, groupd):
81 AstNode.__init__(self, filename, lineno)
82 self.groupd = groupd
83
84 def getFunc(self, key, data):
85 if 'flag' in self.groupd and self.groupd['flag'] != None:
86 return data.getVarFlag(key, self.groupd['flag'], noweakdefault=True)
87 else:
88 return data.getVar(key, noweakdefault=True)
89
90 def eval(self, data):
91 groupd = self.groupd
92 key = groupd["var"]
93 loginfo = {
94 'variable': key,
95 'file': self.filename,
96 'line': self.lineno,
97 }
98 if "exp" in groupd and groupd["exp"] != None:
99 data.setVarFlag(key, "export", 1, op = 'exported', **loginfo)
100
101 op = "set"
102 if "ques" in groupd and groupd["ques"] != None:
103 val = self.getFunc(key, data)
104 op = "set?"
105 if val == None:
106 val = groupd["value"]
107 elif "colon" in groupd and groupd["colon"] != None:
108 e = data.createCopy()
109 bb.data.update_data(e)
110 op = "immediate"
111 val = e.expand(groupd["value"], key + "[:=]")
112 elif "append" in groupd and groupd["append"] != None:
113 op = "append"
114 val = "%s %s" % ((self.getFunc(key, data) or ""), groupd["value"])
115 elif "prepend" in groupd and groupd["prepend"] != None:
116 op = "prepend"
117 val = "%s %s" % (groupd["value"], (self.getFunc(key, data) or ""))
118 elif "postdot" in groupd and groupd["postdot"] != None:
119 op = "postdot"
120 val = "%s%s" % ((self.getFunc(key, data) or ""), groupd["value"])
121 elif "predot" in groupd and groupd["predot"] != None:
122 op = "predot"
123 val = "%s%s" % (groupd["value"], (self.getFunc(key, data) or ""))
124 else:
125 val = groupd["value"]
126
127 flag = None
128 if 'flag' in groupd and groupd['flag'] != None:
129 flag = groupd['flag']
130 elif groupd["lazyques"]:
131 flag = "defaultval"
132
133 loginfo['op'] = op
134 loginfo['detail'] = groupd["value"]
135
136 if flag:
137 data.setVarFlag(key, flag, val, **loginfo)
138 else:
139 data.setVar(key, val, **loginfo)
140
141class MethodNode(AstNode):
142 def __init__(self, filename, lineno, func_name, body):
143 AstNode.__init__(self, filename, lineno)
144 self.func_name = func_name
145 self.body = body
146
147 def eval(self, data):
148 text = '\n'.join(self.body)
149 if self.func_name == "__anonymous":
150 funcname = ("__anon_%s_%s" % (self.lineno, self.filename.translate(string.maketrans('/.+-@', '_____'))))
151 text = "def %s(d):\n" % (funcname) + text
152 bb.methodpool.insert_method(funcname, text, self.filename)
153 anonfuncs = data.getVar('__BBANONFUNCS') or []
154 anonfuncs.append(funcname)
155 data.setVar('__BBANONFUNCS', anonfuncs)
156 data.setVar(funcname, text)
157 else:
158 data.setVarFlag(self.func_name, "func", 1)
159 data.setVar(self.func_name, text)
160
161class PythonMethodNode(AstNode):
162 def __init__(self, filename, lineno, function, modulename, body):
163 AstNode.__init__(self, filename, lineno)
164 self.function = function
165 self.modulename = modulename
166 self.body = body
167
168 def eval(self, data):
169 # Note we will add root to parsedmethods after having parse
170 # 'this' file. This means we will not parse methods from
171 # bb classes twice
172 text = '\n'.join(self.body)
173 bb.methodpool.insert_method(self.modulename, text, self.filename)
174 data.setVarFlag(self.function, "func", 1)
175 data.setVarFlag(self.function, "python", 1)
176 data.setVar(self.function, text)
177
178class MethodFlagsNode(AstNode):
179 def __init__(self, filename, lineno, key, m):
180 AstNode.__init__(self, filename, lineno)
181 self.key = key
182 self.m = m
183
184 def eval(self, data):
185 if data.getVar(self.key):
186 # clean up old version of this piece of metadata, as its
187 # flags could cause problems
188 data.setVarFlag(self.key, 'python', None)
189 data.setVarFlag(self.key, 'fakeroot', None)
190 if self.m.group("py") is not None:
191 data.setVarFlag(self.key, "python", "1")
192 else:
193 data.delVarFlag(self.key, "python")
194 if self.m.group("fr") is not None:
195 data.setVarFlag(self.key, "fakeroot", "1")
196 else:
197 data.delVarFlag(self.key, "fakeroot")
198
199class ExportFuncsNode(AstNode):
200 def __init__(self, filename, lineno, fns, classname):
201 AstNode.__init__(self, filename, lineno)
202 self.n = fns.split()
203 self.classname = classname
204
205 def eval(self, data):
206
207 for func in self.n:
208 calledfunc = self.classname + "_" + func
209
210 if data.getVar(func) and not data.getVarFlag(func, 'export_func'):
211 continue
212
213 if data.getVar(func):
214 data.setVarFlag(func, 'python', None)
215 data.setVarFlag(func, 'func', None)
216
217 for flag in [ "func", "python" ]:
218 if data.getVarFlag(calledfunc, flag):
219 data.setVarFlag(func, flag, data.getVarFlag(calledfunc, flag))
220 for flag in [ "dirs" ]:
221 if data.getVarFlag(func, flag):
222 data.setVarFlag(calledfunc, flag, data.getVarFlag(func, flag))
223
224 if data.getVarFlag(calledfunc, "python"):
225 data.setVar(func, " bb.build.exec_func('" + calledfunc + "', d)\n")
226 else:
227 data.setVar(func, " " + calledfunc + "\n")
228 data.setVarFlag(func, 'export_func', '1')
229
230class AddTaskNode(AstNode):
231 def __init__(self, filename, lineno, func, before, after):
232 AstNode.__init__(self, filename, lineno)
233 self.func = func
234 self.before = before
235 self.after = after
236
237 def eval(self, data):
238 var = self.func
239 if self.func[:3] != "do_":
240 var = "do_" + self.func
241
242 data.setVarFlag(var, "task", 1)
243 bbtasks = data.getVar('__BBTASKS') or []
244 if not var in bbtasks:
245 bbtasks.append(var)
246 data.setVar('__BBTASKS', bbtasks)
247
248 existing = data.getVarFlag(var, "deps") or []
249 if self.after is not None:
250 # set up deps for function
251 for entry in self.after.split():
252 if entry not in existing:
253 existing.append(entry)
254 data.setVarFlag(var, "deps", existing)
255 if self.before is not None:
256 # set up things that depend on this func
257 for entry in self.before.split():
258 existing = data.getVarFlag(entry, "deps") or []
259 if var not in existing:
260 data.setVarFlag(entry, "deps", [var] + existing)
261
262class BBHandlerNode(AstNode):
263 def __init__(self, filename, lineno, fns):
264 AstNode.__init__(self, filename, lineno)
265 self.hs = fns.split()
266
267 def eval(self, data):
268 bbhands = data.getVar('__BBHANDLERS') or []
269 for h in self.hs:
270 bbhands.append(h)
271 data.setVarFlag(h, "handler", 1)
272 data.setVar('__BBHANDLERS', bbhands)
273
274class InheritNode(AstNode):
275 def __init__(self, filename, lineno, classes):
276 AstNode.__init__(self, filename, lineno)
277 self.classes = classes
278
279 def eval(self, data):
280 bb.parse.BBHandler.inherit(self.classes, self.filename, self.lineno, data)
281
282def handleInclude(statements, filename, lineno, m, force):
283 statements.append(IncludeNode(filename, lineno, m.group(1), force))
284
285def handleExport(statements, filename, lineno, m):
286 statements.append(ExportNode(filename, lineno, m.group(1)))
287
288def handleData(statements, filename, lineno, groupd):
289 statements.append(DataNode(filename, lineno, groupd))
290
291def handleMethod(statements, filename, lineno, func_name, body):
292 statements.append(MethodNode(filename, lineno, func_name, body))
293
294def handlePythonMethod(statements, filename, lineno, funcname, modulename, body):
295 statements.append(PythonMethodNode(filename, lineno, funcname, modulename, body))
296
297def handleMethodFlags(statements, filename, lineno, key, m):
298 statements.append(MethodFlagsNode(filename, lineno, key, m))
299
300def handleExportFuncs(statements, filename, lineno, m, classname):
301 statements.append(ExportFuncsNode(filename, lineno, m.group(1), classname))
302
303def handleAddTask(statements, filename, lineno, m):
304 func = m.group("func")
305 before = m.group("before")
306 after = m.group("after")
307 if func is None:
308 return
309
310 statements.append(AddTaskNode(filename, lineno, func, before, after))
311
312def handleBBHandlers(statements, filename, lineno, m):
313 statements.append(BBHandlerNode(filename, lineno, m.group(1)))
314
315def handleInherit(statements, filename, lineno, m):
316 classes = m.group(1)
317 statements.append(InheritNode(filename, lineno, classes))
318
319def finalize(fn, d, variant = None):
320 all_handlers = {}
321 for var in d.getVar('__BBHANDLERS') or []:
322 # try to add the handler
323 bb.event.register(var, d.getVar(var), (d.getVarFlag(var, "eventmask", True) or "").split())
324
325 bb.event.fire(bb.event.RecipePreFinalise(fn), d)
326
327 bb.data.expandKeys(d)
328 bb.data.update_data(d)
329 code = []
330 for funcname in d.getVar("__BBANONFUNCS") or []:
331 code.append("%s(d)" % funcname)
332 bb.utils.better_exec("\n".join(code), {"d": d})
333 bb.data.update_data(d)
334
335 tasklist = d.getVar('__BBTASKS') or []
336 bb.build.add_tasks(tasklist, d)
337
338 bb.parse.siggen.finalise(fn, d, variant)
339
340 d.setVar('BBINCLUDED', bb.parse.get_file_depends(d))
341
342 bb.event.fire(bb.event.RecipeParsed(fn), d)
343
344def _create_variants(datastores, names, function):
345 def create_variant(name, orig_d, arg = None):
346 new_d = bb.data.createCopy(orig_d)
347 function(arg or name, new_d)
348 datastores[name] = new_d
349
350 for variant, variant_d in datastores.items():
351 for name in names:
352 if not variant:
353 # Based on main recipe
354 create_variant(name, variant_d)
355 else:
356 create_variant("%s-%s" % (variant, name), variant_d, name)
357
358def _expand_versions(versions):
359 def expand_one(version, start, end):
360 for i in xrange(start, end + 1):
361 ver = _bbversions_re.sub(str(i), version, 1)
362 yield ver
363
364 versions = iter(versions)
365 while True:
366 try:
367 version = next(versions)
368 except StopIteration:
369 break
370
371 range_ver = _bbversions_re.search(version)
372 if not range_ver:
373 yield version
374 else:
375 newversions = expand_one(version, int(range_ver.group("from")),
376 int(range_ver.group("to")))
377 versions = itertools.chain(newversions, versions)
378
379def multi_finalize(fn, d):
380 appends = (d.getVar("__BBAPPEND", True) or "").split()
381 for append in appends:
382 logger.debug(2, "Appending .bbappend file %s to %s", append, fn)
383 bb.parse.BBHandler.handle(append, d, True)
384
385 onlyfinalise = d.getVar("__ONLYFINALISE", False)
386
387 safe_d = d
388 d = bb.data.createCopy(safe_d)
389 try:
390 finalize(fn, d)
391 except bb.parse.SkipPackage as e:
392 d.setVar("__SKIPPED", e.args[0])
393 datastores = {"": safe_d}
394
395 versions = (d.getVar("BBVERSIONS", True) or "").split()
396 if versions:
397 pv = orig_pv = d.getVar("PV", True)
398 baseversions = {}
399
400 def verfunc(ver, d, pv_d = None):
401 if pv_d is None:
402 pv_d = d
403
404 overrides = d.getVar("OVERRIDES", True).split(":")
405 pv_d.setVar("PV", ver)
406 overrides.append(ver)
407 bpv = baseversions.get(ver) or orig_pv
408 pv_d.setVar("BPV", bpv)
409 overrides.append(bpv)
410 d.setVar("OVERRIDES", ":".join(overrides))
411
412 versions = list(_expand_versions(versions))
413 for pos, version in enumerate(list(versions)):
414 try:
415 pv, bpv = version.split(":", 2)
416 except ValueError:
417 pass
418 else:
419 versions[pos] = pv
420 baseversions[pv] = bpv
421
422 if pv in versions and not baseversions.get(pv):
423 versions.remove(pv)
424 else:
425 pv = versions.pop()
426
427 # This is necessary because our existing main datastore
428 # has already been finalized with the old PV, we need one
429 # that's been finalized with the new PV.
430 d = bb.data.createCopy(safe_d)
431 verfunc(pv, d, safe_d)
432 try:
433 finalize(fn, d)
434 except bb.parse.SkipPackage as e:
435 d.setVar("__SKIPPED", e.args[0])
436
437 _create_variants(datastores, versions, verfunc)
438
439 extended = d.getVar("BBCLASSEXTEND", True) or ""
440 if extended:
441 # the following is to support bbextends with arguments, for e.g. multilib
442 # an example is as follows:
443 # BBCLASSEXTEND = "multilib:lib32"
444 # it will create foo-lib32, inheriting multilib.bbclass and set
445 # BBEXTENDCURR to "multilib" and BBEXTENDVARIANT to "lib32"
446 extendedmap = {}
447 variantmap = {}
448
449 for ext in extended.split():
450 eext = ext.split(':', 2)
451 if len(eext) > 1:
452 extendedmap[ext] = eext[0]
453 variantmap[ext] = eext[1]
454 else:
455 extendedmap[ext] = ext
456
457 pn = d.getVar("PN", True)
458 def extendfunc(name, d):
459 if name != extendedmap[name]:
460 d.setVar("BBEXTENDCURR", extendedmap[name])
461 d.setVar("BBEXTENDVARIANT", variantmap[name])
462 else:
463 d.setVar("PN", "%s-%s" % (pn, name))
464 bb.parse.BBHandler.inherit(extendedmap[name], fn, 0, d)
465
466 safe_d.setVar("BBCLASSEXTEND", extended)
467 _create_variants(datastores, extendedmap.keys(), extendfunc)
468
469 for variant, variant_d in datastores.iteritems():
470 if variant:
471 try:
472 if not onlyfinalise or variant in onlyfinalise:
473 finalize(fn, variant_d, variant)
474 except bb.parse.SkipPackage as e:
475 variant_d.setVar("__SKIPPED", e.args[0])
476
477 if len(datastores) > 1:
478 variants = filter(None, datastores.iterkeys())
479 safe_d.setVar("__VARIANTS", " ".join(variants))
480
481 datastores[""] = d
482 return datastores
diff --git a/bitbake/lib/bb/parse/parse_py/BBHandler.py b/bitbake/lib/bb/parse/parse_py/BBHandler.py
new file mode 100644
index 0000000000..01f22d3b24
--- /dev/null
+++ b/bitbake/lib/bb/parse/parse_py/BBHandler.py
@@ -0,0 +1,258 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4"""
5 class for handling .bb files
6
7 Reads a .bb file and obtains its metadata
8
9"""
10
11
12# Copyright (C) 2003, 2004 Chris Larson
13# Copyright (C) 2003, 2004 Phil Blundell
14#
15# This program is free software; you can redistribute it and/or modify
16# it under the terms of the GNU General Public License version 2 as
17# published by the Free Software Foundation.
18#
19# This program is distributed in the hope that it will be useful,
20# but WITHOUT ANY WARRANTY; without even the implied warranty of
21# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22# GNU General Public License for more details.
23#
24# You should have received a copy of the GNU General Public License along
25# with this program; if not, write to the Free Software Foundation, Inc.,
26# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
27
28from __future__ import absolute_import
29import re, bb, os
30import logging
31import bb.build, bb.utils
32from bb import data
33
34from . import ConfHandler
35from .. import resolve_file, ast, logger
36from .ConfHandler import include, init
37
38# For compatibility
39bb.deprecate_import(__name__, "bb.parse", ["vars_from_file"])
40
41__func_start_regexp__ = re.compile( r"(((?P<py>python)|(?P<fr>fakeroot))\s*)*(?P<func>[\w\.\-\+\{\}\$]+)?\s*\(\s*\)\s*{$" )
42__inherit_regexp__ = re.compile( r"inherit\s+(.+)" )
43__export_func_regexp__ = re.compile( r"EXPORT_FUNCTIONS\s+(.+)" )
44__addtask_regexp__ = re.compile("addtask\s+(?P<func>\w+)\s*((before\s*(?P<before>((.*(?=after))|(.*))))|(after\s*(?P<after>((.*(?=before))|(.*)))))*")
45__addhandler_regexp__ = re.compile( r"addhandler\s+(.+)" )
46__def_regexp__ = re.compile( r"def\s+(\w+).*:" )
47__python_func_regexp__ = re.compile( r"(\s+.*)|(^$)" )
48
49
50__infunc__ = ""
51__inpython__ = False
52__body__ = []
53__classname__ = ""
54
55cached_statements = {}
56
57# We need to indicate EOF to the feeder. This code is so messy that
58# factoring it out to a close_parse_file method is out of question.
59# We will use the IN_PYTHON_EOF as an indicator to just close the method
60#
61# The two parts using it are tightly integrated anyway
62IN_PYTHON_EOF = -9999999999999
63
64
65
66def supports(fn, d):
67 """Return True if fn has a supported extension"""
68 return os.path.splitext(fn)[-1] in [".bb", ".bbclass", ".inc"]
69
70def inherit(files, fn, lineno, d):
71 __inherit_cache = d.getVar('__inherit_cache') or []
72 files = d.expand(files).split()
73 for file in files:
74 if not os.path.isabs(file) and not file.endswith(".bbclass"):
75 file = os.path.join('classes', '%s.bbclass' % file)
76
77 if not os.path.isabs(file):
78 dname = os.path.dirname(fn)
79 bbpath = "%s:%s" % (dname, d.getVar("BBPATH", True))
80 abs_fn = bb.utils.which(bbpath, file)
81 if abs_fn:
82 file = abs_fn
83
84 if not file in __inherit_cache:
85 logger.log(logging.DEBUG -1, "BB %s:%d: inheriting %s", fn, lineno, file)
86 __inherit_cache.append( file )
87 d.setVar('__inherit_cache', __inherit_cache)
88 include(fn, file, lineno, d, "inherit")
89 __inherit_cache = d.getVar('__inherit_cache') or []
90
91def get_statements(filename, absolute_filename, base_name):
92 global cached_statements
93
94 try:
95 return cached_statements[absolute_filename]
96 except KeyError:
97 file = open(absolute_filename, 'r')
98 statements = ast.StatementGroup()
99
100 lineno = 0
101 while True:
102 lineno = lineno + 1
103 s = file.readline()
104 if not s: break
105 s = s.rstrip()
106 feeder(lineno, s, filename, base_name, statements)
107 file.close()
108 if __inpython__:
109 # add a blank line to close out any python definition
110 feeder(IN_PYTHON_EOF, "", filename, base_name, statements)
111
112 if filename.endswith(".bbclass") or filename.endswith(".inc"):
113 cached_statements[absolute_filename] = statements
114 return statements
115
116def handle(fn, d, include):
117 global __func_start_regexp__, __inherit_regexp__, __export_func_regexp__, __addtask_regexp__, __addhandler_regexp__, __infunc__, __body__, __residue__, __classname__
118 __body__ = []
119 __infunc__ = ""
120 __classname__ = ""
121 __residue__ = []
122
123
124 if include == 0:
125 logger.debug(2, "BB %s: handle(data)", fn)
126 else:
127 logger.debug(2, "BB %s: handle(data, include)", fn)
128
129 base_name = os.path.basename(fn)
130 (root, ext) = os.path.splitext(base_name)
131 init(d)
132
133 if ext == ".bbclass":
134 __classname__ = root
135 __inherit_cache = d.getVar('__inherit_cache') or []
136 if not fn in __inherit_cache:
137 __inherit_cache.append(fn)
138 d.setVar('__inherit_cache', __inherit_cache)
139
140 if include != 0:
141 oldfile = d.getVar('FILE')
142 else:
143 oldfile = None
144
145 abs_fn = resolve_file(fn, d)
146
147 if include:
148 bb.parse.mark_dependency(d, abs_fn)
149
150 # actual loading
151 statements = get_statements(fn, abs_fn, base_name)
152
153 # DONE WITH PARSING... time to evaluate
154 if ext != ".bbclass":
155 d.setVar('FILE', abs_fn)
156
157 try:
158 statements.eval(d)
159 except bb.parse.SkipPackage:
160 bb.data.setVar("__SKIPPED", True, d)
161 if include == 0:
162 return { "" : d }
163
164 if ext != ".bbclass" and include == 0:
165 return ast.multi_finalize(fn, d)
166
167 if oldfile:
168 d.setVar("FILE", oldfile)
169
170 return d
171
172def feeder(lineno, s, fn, root, statements):
173 global __func_start_regexp__, __inherit_regexp__, __export_func_regexp__, __addtask_regexp__, __addhandler_regexp__, __def_regexp__, __python_func_regexp__, __inpython__, __infunc__, __body__, bb, __residue__, __classname__
174 if __infunc__:
175 if s == '}':
176 __body__.append('')
177 ast.handleMethod(statements, fn, lineno, __infunc__, __body__)
178 __infunc__ = ""
179 __body__ = []
180 else:
181 __body__.append(s)
182 return
183
184 if __inpython__:
185 m = __python_func_regexp__.match(s)
186 if m and lineno != IN_PYTHON_EOF:
187 __body__.append(s)
188 return
189 else:
190 ast.handlePythonMethod(statements, fn, lineno, __inpython__,
191 root, __body__)
192 __body__ = []
193 __inpython__ = False
194
195 if lineno == IN_PYTHON_EOF:
196 return
197
198 if s and s[0] == '#':
199 if len(__residue__) != 0 and __residue__[0][0] != "#":
200 bb.fatal("There is a comment on line %s of file %s (%s) which is in the middle of a multiline expression.\nBitbake used to ignore these but no longer does so, please fix your metadata as errors are likely as a result of this change." % (lineno, fn, s))
201
202 if len(__residue__) != 0 and __residue__[0][0] == "#" and (not s or s[0] != "#"):
203 bb.fatal("There is a confusing multiline, partially commented expression on line %s of file %s (%s).\nPlease clarify whether this is all a comment or should be parsed." % (lineno, fn, s))
204
205 if s and s[-1] == '\\':
206 __residue__.append(s[:-1])
207 return
208
209 s = "".join(__residue__) + s
210 __residue__ = []
211
212 # Skip empty lines
213 if s == '':
214 return
215
216 # Skip comments
217 if s[0] == '#':
218 return
219
220 m = __func_start_regexp__.match(s)
221 if m:
222 __infunc__ = m.group("func") or "__anonymous"
223 ast.handleMethodFlags(statements, fn, lineno, __infunc__, m)
224 return
225
226 m = __def_regexp__.match(s)
227 if m:
228 __body__.append(s)
229 __inpython__ = m.group(1)
230
231 return
232
233 m = __export_func_regexp__.match(s)
234 if m:
235 ast.handleExportFuncs(statements, fn, lineno, m, __classname__)
236 return
237
238 m = __addtask_regexp__.match(s)
239 if m:
240 ast.handleAddTask(statements, fn, lineno, m)
241 return
242
243 m = __addhandler_regexp__.match(s)
244 if m:
245 ast.handleBBHandlers(statements, fn, lineno, m)
246 return
247
248 m = __inherit_regexp__.match(s)
249 if m:
250 ast.handleInherit(statements, fn, lineno, m)
251 return
252
253 return ConfHandler.feeder(lineno, s, fn, statements)
254
255# Add us to the handlers list
256from .. import handlers
257handlers.append({'supports': supports, 'handle': handle, 'init': init})
258del handlers
diff --git a/bitbake/lib/bb/parse/parse_py/ConfHandler.py b/bitbake/lib/bb/parse/parse_py/ConfHandler.py
new file mode 100644
index 0000000000..7b30c8acb3
--- /dev/null
+++ b/bitbake/lib/bb/parse/parse_py/ConfHandler.py
@@ -0,0 +1,182 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4"""
5 class for handling configuration data files
6
7 Reads a .conf file and obtains its metadata
8
9"""
10
11# Copyright (C) 2003, 2004 Chris Larson
12# Copyright (C) 2003, 2004 Phil Blundell
13#
14# This program is free software; you can redistribute it and/or modify
15# it under the terms of the GNU General Public License version 2 as
16# published by the Free Software Foundation.
17#
18# This program is distributed in the hope that it will be useful,
19# but WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21# GNU General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License along
24# with this program; if not, write to the Free Software Foundation, Inc.,
25# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26
27import re, os
28import logging
29import bb.utils
30from bb.parse import ParseError, resolve_file, ast, logger
31
32__config_regexp__ = re.compile( r"""
33 ^
34 (?P<exp>export\s*)?
35 (?P<var>[a-zA-Z0-9\-~_+.${}/]+?)
36 (\[(?P<flag>[a-zA-Z0-9\-_+.]+)\])?
37
38 \s* (
39 (?P<colon>:=) |
40 (?P<lazyques>\?\?=) |
41 (?P<ques>\?=) |
42 (?P<append>\+=) |
43 (?P<prepend>=\+) |
44 (?P<predot>=\.) |
45 (?P<postdot>\.=) |
46 =
47 ) \s*
48
49 (?!'[^']*'[^']*'$)
50 (?!\"[^\"]*\"[^\"]*\"$)
51 (?P<apo>['\"])
52 (?P<value>.*)
53 (?P=apo)
54 $
55 """, re.X)
56__include_regexp__ = re.compile( r"include\s+(.+)" )
57__require_regexp__ = re.compile( r"require\s+(.+)" )
58__export_regexp__ = re.compile( r"export\s+([a-zA-Z0-9\-_+.${}/]+)$" )
59
60def init(data):
61 topdir = data.getVar('TOPDIR')
62 if not topdir:
63 data.setVar('TOPDIR', os.getcwd())
64
65
66def supports(fn, d):
67 return fn[-5:] == ".conf"
68
69def include(oldfn, fn, lineno, data, error_out):
70 """
71 error_out: A string indicating the verb (e.g. "include", "inherit") to be
72 used in a ParseError that will be raised if the file to be included could
73 not be included. Specify False to avoid raising an error in this case.
74 """
75 if oldfn == fn: # prevent infinite recursion
76 return None
77
78 import bb
79 fn = data.expand(fn)
80 oldfn = data.expand(oldfn)
81
82 if not os.path.isabs(fn):
83 dname = os.path.dirname(oldfn)
84 bbpath = "%s:%s" % (dname, data.getVar("BBPATH", True))
85 abs_fn = bb.utils.which(bbpath, fn)
86 if abs_fn:
87 fn = abs_fn
88
89 from bb.parse import handle
90 try:
91 ret = handle(fn, data, True)
92 except (IOError, OSError):
93 if error_out:
94 raise ParseError("Could not %(error_out)s file %(fn)s" % vars(), oldfn, lineno)
95 logger.debug(2, "CONF file '%s' not found", fn)
96
97# We have an issue where a UI might want to enforce particular settings such as
98# an empty DISTRO variable. If configuration files do something like assigning
99# a weak default, it turns out to be very difficult to filter out these changes,
100# particularly when the weak default might appear half way though parsing a chain
101# of configuration files. We therefore let the UIs hook into configuration file
102# parsing. This turns out to be a hard problem to solve any other way.
103confFilters = []
104
105def handle(fn, data, include):
106 init(data)
107
108 if include == 0:
109 oldfile = None
110 else:
111 oldfile = data.getVar('FILE')
112
113 abs_fn = resolve_file(fn, data)
114 f = open(abs_fn, 'r')
115
116 if include:
117 bb.parse.mark_dependency(data, abs_fn)
118
119 statements = ast.StatementGroup()
120 lineno = 0
121 while True:
122 lineno = lineno + 1
123 s = f.readline()
124 if not s:
125 break
126 w = s.strip()
127 # skip empty lines
128 if not w:
129 continue
130 s = s.rstrip()
131 while s[-1] == '\\':
132 s2 = f.readline().strip()
133 lineno = lineno + 1
134 if (not s2 or s2 and s2[0] != "#") and s[0] == "#" :
135 bb.fatal("There is a confusing multiline, partially commented expression on line %s of file %s (%s).\nPlease clarify whether this is all a comment or should be parsed." % (lineno, fn, s))
136 s = s[:-1] + s2
137 # skip comments
138 if s[0] == '#':
139 continue
140 feeder(lineno, s, fn, statements)
141
142 # DONE WITH PARSING... time to evaluate
143 data.setVar('FILE', abs_fn)
144 statements.eval(data)
145 if oldfile:
146 data.setVar('FILE', oldfile)
147
148 f.close()
149
150 for f in confFilters:
151 f(fn, data)
152
153 return data
154
155def feeder(lineno, s, fn, statements):
156 m = __config_regexp__.match(s)
157 if m:
158 groupd = m.groupdict()
159 ast.handleData(statements, fn, lineno, groupd)
160 return
161
162 m = __include_regexp__.match(s)
163 if m:
164 ast.handleInclude(statements, fn, lineno, m, False)
165 return
166
167 m = __require_regexp__.match(s)
168 if m:
169 ast.handleInclude(statements, fn, lineno, m, True)
170 return
171
172 m = __export_regexp__.match(s)
173 if m:
174 ast.handleExport(statements, fn, lineno, m)
175 return
176
177 raise ParseError("unparsed line: '%s'" % s, fn, lineno);
178
179# Add us to the handlers list
180from bb.parse import handlers
181handlers.append({'supports': supports, 'handle': handle, 'init': init})
182del handlers
diff --git a/bitbake/lib/bb/parse/parse_py/__init__.py b/bitbake/lib/bb/parse/parse_py/__init__.py
new file mode 100644
index 0000000000..3e658d0de9
--- /dev/null
+++ b/bitbake/lib/bb/parse/parse_py/__init__.py
@@ -0,0 +1,33 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4"""
5BitBake Parsers
6
7File parsers for the BitBake build tools.
8
9"""
10
11# Copyright (C) 2003, 2004 Chris Larson
12# Copyright (C) 2003, 2004 Phil Blundell
13#
14# This program is free software; you can redistribute it and/or modify
15# it under the terms of the GNU General Public License version 2 as
16# published by the Free Software Foundation.
17#
18# This program is distributed in the hope that it will be useful,
19# but WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21# GNU General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License along
24# with this program; if not, write to the Free Software Foundation, Inc.,
25# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26#
27# Based on functions from the base bb module, Copyright 2003 Holger Schurig
28
29from __future__ import absolute_import
30from . import ConfHandler
31from . import BBHandler
32
33__version__ = '1.0'
diff --git a/bitbake/lib/bb/persist_data.py b/bitbake/lib/bb/persist_data.py
new file mode 100644
index 0000000000..994e61b0a6
--- /dev/null
+++ b/bitbake/lib/bb/persist_data.py
@@ -0,0 +1,215 @@
1"""BitBake Persistent Data Store
2
3Used to store data in a central location such that other threads/tasks can
4access them at some future date. Acts as a convenience wrapper around sqlite,
5currently, providing a key/value store accessed by 'domain'.
6"""
7
8# Copyright (C) 2007 Richard Purdie
9# Copyright (C) 2010 Chris Larson <chris_larson@mentor.com>
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License version 2 as
13# published by the Free Software Foundation.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License along
21# with this program; if not, write to the Free Software Foundation, Inc.,
22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
24import collections
25import logging
26import os.path
27import sys
28import warnings
29from bb.compat import total_ordering
30from collections import Mapping
31
32try:
33 import sqlite3
34except ImportError:
35 from pysqlite2 import dbapi2 as sqlite3
36
37sqlversion = sqlite3.sqlite_version_info
38if sqlversion[0] < 3 or (sqlversion[0] == 3 and sqlversion[1] < 3):
39 raise Exception("sqlite3 version 3.3.0 or later is required.")
40
41
42logger = logging.getLogger("BitBake.PersistData")
43if hasattr(sqlite3, 'enable_shared_cache'):
44 try:
45 sqlite3.enable_shared_cache(True)
46 except sqlite3.OperationalError:
47 pass
48
49
50@total_ordering
51class SQLTable(collections.MutableMapping):
52 """Object representing a table/domain in the database"""
53 def __init__(self, cachefile, table):
54 self.cachefile = cachefile
55 self.table = table
56 self.cursor = connect(self.cachefile)
57
58 self._execute("CREATE TABLE IF NOT EXISTS %s(key TEXT, value TEXT);"
59 % table)
60
61 def _execute(self, *query):
62 """Execute a query, waiting to acquire a lock if necessary"""
63 count = 0
64 while True:
65 try:
66 return self.cursor.execute(*query)
67 except sqlite3.OperationalError as exc:
68 if 'database is locked' in str(exc) and count < 500:
69 count = count + 1
70 self.cursor.close()
71 self.cursor = connect(self.cachefile)
72 continue
73 raise
74
75 def __enter__(self):
76 self.cursor.__enter__()
77 return self
78
79 def __exit__(self, *excinfo):
80 self.cursor.__exit__(*excinfo)
81
82 def __getitem__(self, key):
83 data = self._execute("SELECT * from %s where key=?;" %
84 self.table, [key])
85 for row in data:
86 return row[1]
87 raise KeyError(key)
88
89 def __delitem__(self, key):
90 if key not in self:
91 raise KeyError(key)
92 self._execute("DELETE from %s where key=?;" % self.table, [key])
93
94 def __setitem__(self, key, value):
95 if not isinstance(key, basestring):
96 raise TypeError('Only string keys are supported')
97 elif not isinstance(value, basestring):
98 raise TypeError('Only string values are supported')
99
100 data = self._execute("SELECT * from %s where key=?;" %
101 self.table, [key])
102 exists = len(list(data))
103 if exists:
104 self._execute("UPDATE %s SET value=? WHERE key=?;" % self.table,
105 [value, key])
106 else:
107 self._execute("INSERT into %s(key, value) values (?, ?);" %
108 self.table, [key, value])
109
110 def __contains__(self, key):
111 return key in set(self)
112
113 def __len__(self):
114 data = self._execute("SELECT COUNT(key) FROM %s;" % self.table)
115 for row in data:
116 return row[0]
117
118 def __iter__(self):
119 data = self._execute("SELECT key FROM %s;" % self.table)
120 return (row[0] for row in data)
121
122 def __lt__(self, other):
123 if not isinstance(other, Mapping):
124 raise NotImplemented
125
126 return len(self) < len(other)
127
128 def get_by_pattern(self, pattern):
129 data = self._execute("SELECT * FROM %s WHERE key LIKE ?;" %
130 self.table, [pattern])
131 return [row[1] for row in data]
132
133 def values(self):
134 return list(self.itervalues())
135
136 def itervalues(self):
137 data = self._execute("SELECT value FROM %s;" % self.table)
138 return (row[0] for row in data)
139
140 def items(self):
141 return list(self.iteritems())
142
143 def iteritems(self):
144 return self._execute("SELECT * FROM %s;" % self.table)
145
146 def clear(self):
147 self._execute("DELETE FROM %s;" % self.table)
148
149 def has_key(self, key):
150 return key in self
151
152
153class PersistData(object):
154 """Deprecated representation of the bitbake persistent data store"""
155 def __init__(self, d):
156 warnings.warn("Use of PersistData is deprecated. Please use "
157 "persist(domain, d) instead.",
158 category=DeprecationWarning,
159 stacklevel=2)
160
161 self.data = persist(d)
162 logger.debug(1, "Using '%s' as the persistent data cache",
163 self.data.filename)
164
165 def addDomain(self, domain):
166 """
167 Add a domain (pending deprecation)
168 """
169 return self.data[domain]
170
171 def delDomain(self, domain):
172 """
173 Removes a domain and all the data it contains
174 """
175 del self.data[domain]
176
177 def getKeyValues(self, domain):
178 """
179 Return a list of key + value pairs for a domain
180 """
181 return self.data[domain].items()
182
183 def getValue(self, domain, key):
184 """
185 Return the value of a key for a domain
186 """
187 return self.data[domain][key]
188
189 def setValue(self, domain, key, value):
190 """
191 Sets the value of a key for a domain
192 """
193 self.data[domain][key] = value
194
195 def delValue(self, domain, key):
196 """
197 Deletes a key/value pair
198 """
199 del self.data[domain][key]
200
201def connect(database):
202 return sqlite3.connect(database, timeout=5, isolation_level=None)
203
204def persist(domain, d):
205 """Convenience factory for SQLTable objects based upon metadata"""
206 import bb.utils
207 cachedir = (d.getVar("PERSISTENT_DIR", True) or
208 d.getVar("CACHE", True))
209 if not cachedir:
210 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
211 sys.exit(1)
212
213 bb.utils.mkdirhier(cachedir)
214 cachefile = os.path.join(cachedir, "bb_persist_data.sqlite3")
215 return SQLTable(cachefile, domain)
diff --git a/bitbake/lib/bb/process.py b/bitbake/lib/bb/process.py
new file mode 100644
index 0000000000..afc8e9bd49
--- /dev/null
+++ b/bitbake/lib/bb/process.py
@@ -0,0 +1,133 @@
1import logging
2import signal
3import subprocess
4import errno
5import select
6
7logger = logging.getLogger('BitBake.Process')
8
9def subprocess_setup():
10 # Python installs a SIGPIPE handler by default. This is usually not what
11 # non-Python subprocesses expect.
12 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
13
14class CmdError(RuntimeError):
15 def __init__(self, command, msg=None):
16 self.command = command
17 self.msg = msg
18
19 def __str__(self):
20 if not isinstance(self.command, basestring):
21 cmd = subprocess.list2cmdline(self.command)
22 else:
23 cmd = self.command
24
25 msg = "Execution of '%s' failed" % cmd
26 if self.msg:
27 msg += ': %s' % self.msg
28 return msg
29
30class NotFoundError(CmdError):
31 def __str__(self):
32 return CmdError.__str__(self) + ": command not found"
33
34class ExecutionError(CmdError):
35 def __init__(self, command, exitcode, stdout = None, stderr = None):
36 CmdError.__init__(self, command)
37 self.exitcode = exitcode
38 self.stdout = stdout
39 self.stderr = stderr
40
41 def __str__(self):
42 message = ""
43 if self.stderr:
44 message += self.stderr
45 if self.stdout:
46 message += self.stdout
47 if message:
48 message = ":\n" + message
49 return (CmdError.__str__(self) +
50 " with exit code %s" % self.exitcode + message)
51
52class Popen(subprocess.Popen):
53 defaults = {
54 "close_fds": True,
55 "preexec_fn": subprocess_setup,
56 "stdout": subprocess.PIPE,
57 "stderr": subprocess.STDOUT,
58 "stdin": subprocess.PIPE,
59 "shell": False,
60 }
61
62 def __init__(self, *args, **kwargs):
63 options = dict(self.defaults)
64 options.update(kwargs)
65 subprocess.Popen.__init__(self, *args, **options)
66
67def _logged_communicate(pipe, log, input):
68 if pipe.stdin:
69 if input is not None:
70 pipe.stdin.write(input)
71 pipe.stdin.close()
72
73 outdata, errdata = [], []
74 rin = []
75
76 if pipe.stdout is not None:
77 bb.utils.nonblockingfd(pipe.stdout.fileno())
78 rin.append(pipe.stdout)
79 if pipe.stderr is not None:
80 bb.utils.nonblockingfd(pipe.stderr.fileno())
81 rin.append(pipe.stderr)
82
83 try:
84 while pipe.poll() is None:
85 rlist = rin
86 try:
87 r,w,e = select.select (rlist, [], [])
88 except OSError as e:
89 if e.errno != errno.EINTR:
90 raise
91
92 if pipe.stdout in r:
93 data = pipe.stdout.read()
94 if data is not None:
95 outdata.append(data)
96 log.write(data)
97
98 if pipe.stderr in r:
99 data = pipe.stderr.read()
100 if data is not None:
101 errdata.append(data)
102 log.write(data)
103 finally:
104 log.flush()
105 if pipe.stdout is not None:
106 pipe.stdout.close()
107 if pipe.stderr is not None:
108 pipe.stderr.close()
109 return ''.join(outdata), ''.join(errdata)
110
111def run(cmd, input=None, log=None, **options):
112 """Convenience function to run a command and return its output, raising an
113 exception when the command fails"""
114
115 if isinstance(cmd, basestring) and not "shell" in options:
116 options["shell"] = True
117
118 try:
119 pipe = Popen(cmd, **options)
120 except OSError as exc:
121 if exc.errno == 2:
122 raise NotFoundError(cmd)
123 else:
124 raise CmdError(cmd, exc)
125
126 if log:
127 stdout, stderr = _logged_communicate(pipe, log, input)
128 else:
129 stdout, stderr = pipe.communicate(input)
130
131 if pipe.returncode != 0:
132 raise ExecutionError(cmd, pipe.returncode, stdout, stderr)
133 return stdout, stderr
diff --git a/bitbake/lib/bb/providers.py b/bitbake/lib/bb/providers.py
new file mode 100644
index 0000000000..3a4f6040a5
--- /dev/null
+++ b/bitbake/lib/bb/providers.py
@@ -0,0 +1,381 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# Copyright (C) 2003, 2004 Chris Larson
5# Copyright (C) 2003, 2004 Phil Blundell
6# Copyright (C) 2003 - 2005 Michael 'Mickey' Lauer
7# Copyright (C) 2005 Holger Hans Peter Freyther
8# Copyright (C) 2005 ROAD GmbH
9# Copyright (C) 2006 Richard Purdie
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License version 2 as
13# published by the Free Software Foundation.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License along
21# with this program; if not, write to the Free Software Foundation, Inc.,
22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
24import re
25import logging
26from bb import data, utils
27from collections import defaultdict
28import bb
29
30logger = logging.getLogger("BitBake.Provider")
31
32class NoProvider(bb.BBHandledException):
33 """Exception raised when no provider of a build dependency can be found"""
34
35class NoRProvider(bb.BBHandledException):
36 """Exception raised when no provider of a runtime dependency can be found"""
37
38class MultipleRProvider(bb.BBHandledException):
39 """Exception raised when multiple providers of a runtime dependency can be found"""
40
41def findProviders(cfgData, dataCache, pkg_pn = None):
42 """
43 Convenience function to get latest and preferred providers in pkg_pn
44 """
45
46 if not pkg_pn:
47 pkg_pn = dataCache.pkg_pn
48
49 # Need to ensure data store is expanded
50 localdata = data.createCopy(cfgData)
51 bb.data.update_data(localdata)
52 bb.data.expandKeys(localdata)
53
54 preferred_versions = {}
55 latest_versions = {}
56
57 for pn in pkg_pn:
58 (last_ver, last_file, pref_ver, pref_file) = findBestProvider(pn, localdata, dataCache, pkg_pn)
59 preferred_versions[pn] = (pref_ver, pref_file)
60 latest_versions[pn] = (last_ver, last_file)
61
62 return (latest_versions, preferred_versions)
63
64
65def allProviders(dataCache):
66 """
67 Find all providers for each pn
68 """
69 all_providers = defaultdict(list)
70 for (fn, pn) in dataCache.pkg_fn.items():
71 ver = dataCache.pkg_pepvpr[fn]
72 all_providers[pn].append((ver, fn))
73 return all_providers
74
75
76def sortPriorities(pn, dataCache, pkg_pn = None):
77 """
78 Reorder pkg_pn by file priority and default preference
79 """
80
81 if not pkg_pn:
82 pkg_pn = dataCache.pkg_pn
83
84 files = pkg_pn[pn]
85 priorities = {}
86 for f in files:
87 priority = dataCache.bbfile_priority[f]
88 preference = dataCache.pkg_dp[f]
89 if priority not in priorities:
90 priorities[priority] = {}
91 if preference not in priorities[priority]:
92 priorities[priority][preference] = []
93 priorities[priority][preference].append(f)
94 tmp_pn = []
95 for pri in sorted(priorities):
96 tmp_pref = []
97 for pref in sorted(priorities[pri]):
98 tmp_pref.extend(priorities[pri][pref])
99 tmp_pn = [tmp_pref] + tmp_pn
100
101 return tmp_pn
102
103def preferredVersionMatch(pe, pv, pr, preferred_e, preferred_v, preferred_r):
104 """
105 Check if the version pe,pv,pr is the preferred one.
106 If there is preferred version defined and ends with '%', then pv has to start with that version after removing the '%'
107 """
108 if (pr == preferred_r or preferred_r == None):
109 if (pe == preferred_e or preferred_e == None):
110 if preferred_v == pv:
111 return True
112 if preferred_v != None and preferred_v.endswith('%') and pv.startswith(preferred_v[:len(preferred_v)-1]):
113 return True
114 return False
115
116def findPreferredProvider(pn, cfgData, dataCache, pkg_pn = None, item = None):
117 """
118 Find the first provider in pkg_pn with a PREFERRED_VERSION set.
119 """
120
121 preferred_file = None
122 preferred_ver = None
123
124 localdata = data.createCopy(cfgData)
125 localdata.setVar('OVERRIDES', "%s:pn-%s:%s" % (data.getVar('OVERRIDES', localdata), pn, pn))
126 bb.data.update_data(localdata)
127
128 preferred_v = localdata.getVar('PREFERRED_VERSION', True)
129 if preferred_v:
130 m = re.match('(\d+:)*(.*)(_.*)*', preferred_v)
131 if m:
132 if m.group(1):
133 preferred_e = m.group(1)[:-1]
134 else:
135 preferred_e = None
136 preferred_v = m.group(2)
137 if m.group(3):
138 preferred_r = m.group(3)[1:]
139 else:
140 preferred_r = None
141 else:
142 preferred_e = None
143 preferred_r = None
144
145 for file_set in pkg_pn:
146 for f in file_set:
147 pe, pv, pr = dataCache.pkg_pepvpr[f]
148 if preferredVersionMatch(pe, pv, pr, preferred_e, preferred_v, preferred_r):
149 preferred_file = f
150 preferred_ver = (pe, pv, pr)
151 break
152 if preferred_file:
153 break;
154 if preferred_r:
155 pv_str = '%s-%s' % (preferred_v, preferred_r)
156 else:
157 pv_str = preferred_v
158 if not (preferred_e is None):
159 pv_str = '%s:%s' % (preferred_e, pv_str)
160 itemstr = ""
161 if item:
162 itemstr = " (for item %s)" % item
163 if preferred_file is None:
164 logger.info("preferred version %s of %s not available%s", pv_str, pn, itemstr)
165 available_vers = []
166 for file_set in pkg_pn:
167 for f in file_set:
168 pe, pv, pr = dataCache.pkg_pepvpr[f]
169 ver_str = pv
170 if pe:
171 ver_str = "%s:%s" % (pe, ver_str)
172 if not ver_str in available_vers:
173 available_vers.append(ver_str)
174 if available_vers:
175 available_vers.sort()
176 logger.info("versions of %s available: %s", pn, ' '.join(available_vers))
177 else:
178 logger.debug(1, "selecting %s as PREFERRED_VERSION %s of package %s%s", preferred_file, pv_str, pn, itemstr)
179
180 return (preferred_ver, preferred_file)
181
182
183def findLatestProvider(pn, cfgData, dataCache, file_set):
184 """
185 Return the highest version of the providers in file_set.
186 Take default preferences into account.
187 """
188 latest = None
189 latest_p = 0
190 latest_f = None
191 for file_name in file_set:
192 pe, pv, pr = dataCache.pkg_pepvpr[file_name]
193 dp = dataCache.pkg_dp[file_name]
194
195 if (latest is None) or ((latest_p == dp) and (utils.vercmp(latest, (pe, pv, pr)) < 0)) or (dp > latest_p):
196 latest = (pe, pv, pr)
197 latest_f = file_name
198 latest_p = dp
199
200 return (latest, latest_f)
201
202
203def findBestProvider(pn, cfgData, dataCache, pkg_pn = None, item = None):
204 """
205 If there is a PREFERRED_VERSION, find the highest-priority bbfile
206 providing that version. If not, find the latest version provided by
207 an bbfile in the highest-priority set.
208 """
209
210 sortpkg_pn = sortPriorities(pn, dataCache, pkg_pn)
211 # Find the highest priority provider with a PREFERRED_VERSION set
212 (preferred_ver, preferred_file) = findPreferredProvider(pn, cfgData, dataCache, sortpkg_pn, item)
213 # Find the latest version of the highest priority provider
214 (latest, latest_f) = findLatestProvider(pn, cfgData, dataCache, sortpkg_pn[0])
215
216 if preferred_file is None:
217 preferred_file = latest_f
218 preferred_ver = latest
219
220 return (latest, latest_f, preferred_ver, preferred_file)
221
222
223def _filterProviders(providers, item, cfgData, dataCache):
224 """
225 Take a list of providers and filter/reorder according to the
226 environment variables and previous build results
227 """
228 eligible = []
229 preferred_versions = {}
230 sortpkg_pn = {}
231
232 # The order of providers depends on the order of the files on the disk
233 # up to here. Sort pkg_pn to make dependency issues reproducible rather
234 # than effectively random.
235 providers.sort()
236
237 # Collate providers by PN
238 pkg_pn = {}
239 for p in providers:
240 pn = dataCache.pkg_fn[p]
241 if pn not in pkg_pn:
242 pkg_pn[pn] = []
243 pkg_pn[pn].append(p)
244
245 logger.debug(1, "providers for %s are: %s", item, pkg_pn.keys())
246
247 # First add PREFERRED_VERSIONS
248 for pn in pkg_pn:
249 sortpkg_pn[pn] = sortPriorities(pn, dataCache, pkg_pn)
250 preferred_versions[pn] = findPreferredProvider(pn, cfgData, dataCache, sortpkg_pn[pn], item)
251 if preferred_versions[pn][1]:
252 eligible.append(preferred_versions[pn][1])
253
254 # Now add latest versions
255 for pn in sortpkg_pn:
256 if pn in preferred_versions and preferred_versions[pn][1]:
257 continue
258 preferred_versions[pn] = findLatestProvider(pn, cfgData, dataCache, sortpkg_pn[pn][0])
259 eligible.append(preferred_versions[pn][1])
260
261 if len(eligible) == 0:
262 logger.error("no eligible providers for %s", item)
263 return 0
264
265 # If pn == item, give it a slight default preference
266 # This means PREFERRED_PROVIDER_foobar defaults to foobar if available
267 for p in providers:
268 pn = dataCache.pkg_fn[p]
269 if pn != item:
270 continue
271 (newvers, fn) = preferred_versions[pn]
272 if not fn in eligible:
273 continue
274 eligible.remove(fn)
275 eligible = [fn] + eligible
276
277 return eligible
278
279
280def filterProviders(providers, item, cfgData, dataCache):
281 """
282 Take a list of providers and filter/reorder according to the
283 environment variables and previous build results
284 Takes a "normal" target item
285 """
286
287 eligible = _filterProviders(providers, item, cfgData, dataCache)
288
289 prefervar = cfgData.getVar('PREFERRED_PROVIDER_%s' % item, True)
290 if prefervar:
291 dataCache.preferred[item] = prefervar
292
293 foundUnique = False
294 if item in dataCache.preferred:
295 for p in eligible:
296 pn = dataCache.pkg_fn[p]
297 if dataCache.preferred[item] == pn:
298 logger.verbose("selecting %s to satisfy %s due to PREFERRED_PROVIDERS", pn, item)
299 eligible.remove(p)
300 eligible = [p] + eligible
301 foundUnique = True
302 break
303
304 logger.debug(1, "sorted providers for %s are: %s", item, eligible)
305
306 return eligible, foundUnique
307
308def filterProvidersRunTime(providers, item, cfgData, dataCache):
309 """
310 Take a list of providers and filter/reorder according to the
311 environment variables and previous build results
312 Takes a "runtime" target item
313 """
314
315 eligible = _filterProviders(providers, item, cfgData, dataCache)
316
317 # Should use dataCache.preferred here?
318 preferred = []
319 preferred_vars = []
320 pns = {}
321 for p in eligible:
322 pns[dataCache.pkg_fn[p]] = p
323 for p in eligible:
324 pn = dataCache.pkg_fn[p]
325 provides = dataCache.pn_provides[pn]
326 for provide in provides:
327 prefervar = cfgData.getVar('PREFERRED_PROVIDER_%s' % provide, True)
328 logger.debug(1, "checking PREFERRED_PROVIDER_%s (value %s) against %s", provide, prefervar, pns.keys())
329 if prefervar in pns and pns[prefervar] not in preferred:
330 var = "PREFERRED_PROVIDER_%s = %s" % (provide, prefervar)
331 logger.verbose("selecting %s to satisfy runtime %s due to %s", prefervar, item, var)
332 preferred_vars.append(var)
333 pref = pns[prefervar]
334 eligible.remove(pref)
335 eligible = [pref] + eligible
336 preferred.append(pref)
337 break
338
339 numberPreferred = len(preferred)
340
341 if numberPreferred > 1:
342 logger.error("Trying to resolve runtime dependency %s resulted in conflicting PREFERRED_PROVIDER entries being found.\nThe providers found were: %s\nThe PREFERRED_PROVIDER entries resulting in this conflict were: %s", item, preferred, preferred_vars)
343
344 logger.debug(1, "sorted runtime providers for %s are: %s", item, eligible)
345
346 return eligible, numberPreferred
347
348regexp_cache = {}
349
350def getRuntimeProviders(dataCache, rdepend):
351 """
352 Return any providers of runtime dependency
353 """
354 rproviders = []
355
356 if rdepend in dataCache.rproviders:
357 rproviders += dataCache.rproviders[rdepend]
358
359 if rdepend in dataCache.packages:
360 rproviders += dataCache.packages[rdepend]
361
362 if rproviders:
363 return rproviders
364
365 # Only search dynamic packages if we can't find anything in other variables
366 for pattern in dataCache.packages_dynamic:
367 pattern = pattern.replace('+', "\+")
368 if pattern in regexp_cache:
369 regexp = regexp_cache[pattern]
370 else:
371 try:
372 regexp = re.compile(pattern)
373 except:
374 logger.error("Error parsing regular expression '%s'", pattern)
375 raise
376 regexp_cache[pattern] = regexp
377 if regexp.match(rdepend):
378 rproviders += dataCache.packages_dynamic[pattern]
379 logger.debug(1, "Assuming %s is a dynamic package, but it may not exist" % rdepend)
380
381 return rproviders
diff --git a/bitbake/lib/bb/pysh/__init__.py b/bitbake/lib/bb/pysh/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/bitbake/lib/bb/pysh/__init__.py
diff --git a/bitbake/lib/bb/pysh/builtin.py b/bitbake/lib/bb/pysh/builtin.py
new file mode 100644
index 0000000000..b748e4a4f2
--- /dev/null
+++ b/bitbake/lib/bb/pysh/builtin.py
@@ -0,0 +1,710 @@
1# builtin.py - builtins and utilities definitions for pysh.
2#
3# Copyright 2007 Patrick Mezard
4#
5# This software may be used and distributed according to the terms
6# of the GNU General Public License, incorporated herein by reference.
7
8"""Builtin and internal utilities implementations.
9
10- Beware not to use python interpreter environment as if it were the shell
11environment. For instance, commands working directory must be explicitely handled
12through env['PWD'] instead of relying on python working directory.
13"""
14import errno
15import optparse
16import os
17import re
18import subprocess
19import sys
20import time
21
22def has_subprocess_bug():
23 return getattr(subprocess, 'list2cmdline') and \
24 ( subprocess.list2cmdline(['']) == '' or \
25 subprocess.list2cmdline(['foo|bar']) == 'foo|bar')
26
27# Detect python bug 1634343: "subprocess swallows empty arguments under win32"
28# <http://sourceforge.net/tracker/index.php?func=detail&aid=1634343&group_id=5470&atid=105470>
29# Also detect: "[ 1710802 ] subprocess must escape redirection characters under win32"
30# <http://sourceforge.net/tracker/index.php?func=detail&aid=1710802&group_id=5470&atid=105470>
31if has_subprocess_bug():
32 import subprocess_fix
33 subprocess.list2cmdline = subprocess_fix.list2cmdline
34
35from sherrors import *
36
37class NonExitingParser(optparse.OptionParser):
38 """OptionParser default behaviour upon error is to print the error message and
39 exit. Raise a utility error instead.
40 """
41 def error(self, msg):
42 raise UtilityError(msg)
43
44#-------------------------------------------------------------------------------
45# set special builtin
46#-------------------------------------------------------------------------------
47OPT_SET = NonExitingParser(usage="set - set or unset options and positional parameters")
48OPT_SET.add_option( '-f', action='store_true', dest='has_f', default=False,
49 help='The shell shall disable pathname expansion.')
50OPT_SET.add_option('-e', action='store_true', dest='has_e', default=False,
51 help="""When this option is on, if a simple command fails for any of the \
52 reasons listed in Consequences of Shell Errors or returns an exit status \
53 value >0, and is not part of the compound list following a while, until, \
54 or if keyword, and is not a part of an AND or OR list, and is not a \
55 pipeline preceded by the ! reserved word, then the shell shall immediately \
56 exit.""")
57OPT_SET.add_option('-x', action='store_true', dest='has_x', default=False,
58 help="""The shell shall write to standard error a trace for each command \
59 after it expands the command and before it executes it. It is unspecified \
60 whether the command that turns tracing off is traced.""")
61
62def builtin_set(name, args, interp, env, stdin, stdout, stderr, debugflags):
63 if 'debug-utility' in debugflags:
64 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
65
66 option, args = OPT_SET.parse_args(args)
67 env = interp.get_env()
68
69 if option.has_f:
70 env.set_opt('-f')
71 if option.has_e:
72 env.set_opt('-e')
73 if option.has_x:
74 env.set_opt('-x')
75 return 0
76
77#-------------------------------------------------------------------------------
78# shift special builtin
79#-------------------------------------------------------------------------------
80def builtin_shift(name, args, interp, env, stdin, stdout, stderr, debugflags):
81 if 'debug-utility' in debugflags:
82 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
83
84 params = interp.get_env().get_positional_args()
85 if args:
86 try:
87 n = int(args[0])
88 if n > len(params):
89 raise ValueError()
90 except ValueError:
91 return 1
92 else:
93 n = 1
94
95 params[:n] = []
96 interp.get_env().set_positional_args(params)
97 return 0
98
99#-------------------------------------------------------------------------------
100# export special builtin
101#-------------------------------------------------------------------------------
102OPT_EXPORT = NonExitingParser(usage="set - set or unset options and positional parameters")
103OPT_EXPORT.add_option('-p', action='store_true', dest='has_p', default=False)
104
105def builtin_export(name, args, interp, env, stdin, stdout, stderr, debugflags):
106 if 'debug-utility' in debugflags:
107 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
108
109 option, args = OPT_EXPORT.parse_args(args)
110 if option.has_p:
111 raise NotImplementedError()
112
113 for arg in args:
114 try:
115 name, value = arg.split('=', 1)
116 except ValueError:
117 name, value = arg, None
118 env = interp.get_env().export(name, value)
119
120 return 0
121
122#-------------------------------------------------------------------------------
123# return special builtin
124#-------------------------------------------------------------------------------
125def builtin_return(name, args, interp, env, stdin, stdout, stderr, debugflags):
126 if 'debug-utility' in debugflags:
127 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
128 res = 0
129 if args:
130 try:
131 res = int(args[0])
132 except ValueError:
133 res = 0
134 if not 0<=res<=255:
135 res = 0
136
137 # BUG: should be last executed command exit code
138 raise ReturnSignal(res)
139
140#-------------------------------------------------------------------------------
141# trap special builtin
142#-------------------------------------------------------------------------------
143def builtin_trap(name, args, interp, env, stdin, stdout, stderr, debugflags):
144 if 'debug-utility' in debugflags:
145 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
146 if len(args) < 2:
147 stderr.write('trap: usage: trap [[arg] signal_spec ...]\n')
148 return 2
149
150 action = args[0]
151 for sig in args[1:]:
152 try:
153 env.traps[sig] = action
154 except Exception as e:
155 stderr.write('trap: %s\n' % str(e))
156 return 0
157
158#-------------------------------------------------------------------------------
159# unset special builtin
160#-------------------------------------------------------------------------------
161OPT_UNSET = NonExitingParser("unset - unset values and attributes of variables and functions")
162OPT_UNSET.add_option( '-f', action='store_true', dest='has_f', default=False)
163OPT_UNSET.add_option( '-v', action='store_true', dest='has_v', default=False)
164
165def builtin_unset(name, args, interp, env, stdin, stdout, stderr, debugflags):
166 if 'debug-utility' in debugflags:
167 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
168
169 option, args = OPT_UNSET.parse_args(args)
170
171 status = 0
172 env = interp.get_env()
173 for arg in args:
174 try:
175 if option.has_f:
176 env.remove_function(arg)
177 else:
178 del env[arg]
179 except KeyError:
180 pass
181 except VarAssignmentError:
182 status = 1
183
184 return status
185
186#-------------------------------------------------------------------------------
187# wait special builtin
188#-------------------------------------------------------------------------------
189def builtin_wait(name, args, interp, env, stdin, stdout, stderr, debugflags):
190 if 'debug-utility' in debugflags:
191 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
192
193 return interp.wait([int(arg) for arg in args])
194
195#-------------------------------------------------------------------------------
196# cat utility
197#-------------------------------------------------------------------------------
198def utility_cat(name, args, interp, env, stdin, stdout, stderr, debugflags):
199 if 'debug-utility' in debugflags:
200 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
201
202 if not args:
203 args = ['-']
204
205 status = 0
206 for arg in args:
207 if arg == '-':
208 data = stdin.read()
209 else:
210 path = os.path.join(env['PWD'], arg)
211 try:
212 f = file(path, 'rb')
213 try:
214 data = f.read()
215 finally:
216 f.close()
217 except IOError as e:
218 if e.errno != errno.ENOENT:
219 raise
220 status = 1
221 continue
222 stdout.write(data)
223 stdout.flush()
224 return status
225
226#-------------------------------------------------------------------------------
227# cd utility
228#-------------------------------------------------------------------------------
229OPT_CD = NonExitingParser("cd - change the working directory")
230
231def utility_cd(name, args, interp, env, stdin, stdout, stderr, debugflags):
232 if 'debug-utility' in debugflags:
233 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
234
235 option, args = OPT_CD.parse_args(args)
236 env = interp.get_env()
237
238 directory = None
239 printdir = False
240 if not args:
241 home = env.get('HOME')
242 if home:
243 # Unspecified, do nothing
244 return 0
245 else:
246 directory = home
247 elif len(args)==1:
248 directory = args[0]
249 if directory=='-':
250 if 'OLDPWD' not in env:
251 raise UtilityError("OLDPWD not set")
252 printdir = True
253 directory = env['OLDPWD']
254 else:
255 raise UtilityError("too many arguments")
256
257 curpath = None
258 # Absolute directories will be handled correctly by the os.path.join call.
259 if not directory.startswith('.') and not directory.startswith('..'):
260 cdpaths = env.get('CDPATH', '.').split(';')
261 for cdpath in cdpaths:
262 p = os.path.join(cdpath, directory)
263 if os.path.isdir(p):
264 curpath = p
265 break
266
267 if curpath is None:
268 curpath = directory
269 curpath = os.path.join(env['PWD'], directory)
270
271 env['OLDPWD'] = env['PWD']
272 env['PWD'] = curpath
273 if printdir:
274 stdout.write('%s\n' % curpath)
275 return 0
276
277#-------------------------------------------------------------------------------
278# colon utility
279#-------------------------------------------------------------------------------
280def utility_colon(name, args, interp, env, stdin, stdout, stderr, debugflags):
281 if 'debug-utility' in debugflags:
282 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
283 return 0
284
285#-------------------------------------------------------------------------------
286# echo utility
287#-------------------------------------------------------------------------------
288def utility_echo(name, args, interp, env, stdin, stdout, stderr, debugflags):
289 if 'debug-utility' in debugflags:
290 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
291
292 # Echo only takes arguments, no options. Use printf if you need fancy stuff.
293 output = ' '.join(args) + '\n'
294 stdout.write(output)
295 stdout.flush()
296 return 0
297
298#-------------------------------------------------------------------------------
299# egrep utility
300#-------------------------------------------------------------------------------
301# egrep is usually a shell script.
302# Unfortunately, pysh does not support shell scripts *with arguments* right now,
303# so the redirection is implemented here, assuming grep is available.
304def utility_egrep(name, args, interp, env, stdin, stdout, stderr, debugflags):
305 if 'debug-utility' in debugflags:
306 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
307
308 return run_command('grep', ['-E'] + args, interp, env, stdin, stdout,
309 stderr, debugflags)
310
311#-------------------------------------------------------------------------------
312# env utility
313#-------------------------------------------------------------------------------
314def utility_env(name, args, interp, env, stdin, stdout, stderr, debugflags):
315 if 'debug-utility' in debugflags:
316 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
317
318 if args and args[0]=='-i':
319 raise NotImplementedError('env: -i option is not implemented')
320
321 i = 0
322 for arg in args:
323 if '=' not in arg:
324 break
325 # Update the current environment
326 name, value = arg.split('=', 1)
327 env[name] = value
328 i += 1
329
330 if args[i:]:
331 # Find then execute the specified interpreter
332 utility = env.find_in_path(args[i])
333 if not utility:
334 return 127
335 args[i:i+1] = utility
336 name = args[i]
337 args = args[i+1:]
338 try:
339 return run_command(name, args, interp, env, stdin, stdout, stderr,
340 debugflags)
341 except UtilityError:
342 stderr.write('env: failed to execute %s' % ' '.join([name]+args))
343 return 126
344 else:
345 for pair in env.get_variables().iteritems():
346 stdout.write('%s=%s\n' % pair)
347 return 0
348
349#-------------------------------------------------------------------------------
350# exit utility
351#-------------------------------------------------------------------------------
352def utility_exit(name, args, interp, env, stdin, stdout, stderr, debugflags):
353 if 'debug-utility' in debugflags:
354 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
355
356 res = None
357 if args:
358 try:
359 res = int(args[0])
360 except ValueError:
361 res = None
362 if not 0<=res<=255:
363 res = None
364
365 if res is None:
366 # BUG: should be last executed command exit code
367 res = 0
368
369 raise ExitSignal(res)
370
371#-------------------------------------------------------------------------------
372# fgrep utility
373#-------------------------------------------------------------------------------
374# see egrep
375def utility_fgrep(name, args, interp, env, stdin, stdout, stderr, debugflags):
376 if 'debug-utility' in debugflags:
377 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
378
379 return run_command('grep', ['-F'] + args, interp, env, stdin, stdout,
380 stderr, debugflags)
381
382#-------------------------------------------------------------------------------
383# gunzip utility
384#-------------------------------------------------------------------------------
385# see egrep
386def utility_gunzip(name, args, interp, env, stdin, stdout, stderr, debugflags):
387 if 'debug-utility' in debugflags:
388 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
389
390 return run_command('gzip', ['-d'] + args, interp, env, stdin, stdout,
391 stderr, debugflags)
392
393#-------------------------------------------------------------------------------
394# kill utility
395#-------------------------------------------------------------------------------
396def utility_kill(name, args, interp, env, stdin, stdout, stderr, debugflags):
397 if 'debug-utility' in debugflags:
398 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
399
400 for arg in args:
401 pid = int(arg)
402 status = subprocess.call(['pskill', '/T', str(pid)],
403 shell=True,
404 stdout=subprocess.PIPE,
405 stderr=subprocess.PIPE)
406 # pskill is asynchronous, hence the stupid polling loop
407 while 1:
408 p = subprocess.Popen(['pslist', str(pid)],
409 shell=True,
410 stdout=subprocess.PIPE,
411 stderr=subprocess.STDOUT)
412 output = p.communicate()[0]
413 if ('process %d was not' % pid) in output:
414 break
415 time.sleep(1)
416 return status
417
418#-------------------------------------------------------------------------------
419# mkdir utility
420#-------------------------------------------------------------------------------
421OPT_MKDIR = NonExitingParser("mkdir - make directories.")
422OPT_MKDIR.add_option('-p', action='store_true', dest='has_p', default=False)
423
424def utility_mkdir(name, args, interp, env, stdin, stdout, stderr, debugflags):
425 if 'debug-utility' in debugflags:
426 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
427
428 # TODO: implement umask
429 # TODO: implement proper utility error report
430 option, args = OPT_MKDIR.parse_args(args)
431 for arg in args:
432 path = os.path.join(env['PWD'], arg)
433 if option.has_p:
434 try:
435 os.makedirs(path)
436 except IOError as e:
437 if e.errno != errno.EEXIST:
438 raise
439 else:
440 os.mkdir(path)
441 return 0
442
443#-------------------------------------------------------------------------------
444# netstat utility
445#-------------------------------------------------------------------------------
446def utility_netstat(name, args, interp, env, stdin, stdout, stderr, debugflags):
447 # Do you really expect me to implement netstat ?
448 # This empty form is enough for Mercurial tests since it's
449 # supposed to generate nothing upon success. Faking this test
450 # is not a big deal either.
451 if 'debug-utility' in debugflags:
452 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
453 return 0
454
455#-------------------------------------------------------------------------------
456# pwd utility
457#-------------------------------------------------------------------------------
458OPT_PWD = NonExitingParser("pwd - return working directory name")
459OPT_PWD.add_option('-L', action='store_true', dest='has_L', default=True,
460 help="""If the PWD environment variable contains an absolute pathname of \
461 the current directory that does not contain the filenames dot or dot-dot, \
462 pwd shall write this pathname to standard output. Otherwise, the -L option \
463 shall behave as the -P option.""")
464OPT_PWD.add_option('-P', action='store_true', dest='has_L', default=False,
465 help="""The absolute pathname written shall not contain filenames that, in \
466 the context of the pathname, refer to files of type symbolic link.""")
467
468def utility_pwd(name, args, interp, env, stdin, stdout, stderr, debugflags):
469 if 'debug-utility' in debugflags:
470 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
471
472 option, args = OPT_PWD.parse_args(args)
473 stdout.write('%s\n' % env['PWD'])
474 return 0
475
476#-------------------------------------------------------------------------------
477# printf utility
478#-------------------------------------------------------------------------------
479RE_UNESCAPE = re.compile(r'(\\x[a-zA-Z0-9]{2}|\\[0-7]{1,3}|\\.)')
480
481def utility_printf(name, args, interp, env, stdin, stdout, stderr, debugflags):
482 if 'debug-utility' in debugflags:
483 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
484
485 def replace(m):
486 assert m.group()
487 g = m.group()[1:]
488 if g.startswith('x'):
489 return chr(int(g[1:], 16))
490 if len(g) <= 3 and len([c for c in g if c in '01234567']) == len(g):
491 # Yay, an octal number
492 return chr(int(g, 8))
493 return {
494 'a': '\a',
495 'b': '\b',
496 'f': '\f',
497 'n': '\n',
498 'r': '\r',
499 't': '\t',
500 'v': '\v',
501 '\\': '\\',
502 }.get(g)
503
504 # Convert escape sequences
505 format = re.sub(RE_UNESCAPE, replace, args[0])
506 stdout.write(format % tuple(args[1:]))
507 return 0
508
509#-------------------------------------------------------------------------------
510# true utility
511#-------------------------------------------------------------------------------
512def utility_true(name, args, interp, env, stdin, stdout, stderr, debugflags):
513 if 'debug-utility' in debugflags:
514 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
515 return 0
516
517#-------------------------------------------------------------------------------
518# sed utility
519#-------------------------------------------------------------------------------
520RE_SED = re.compile(r'^s(.).*\1[a-zA-Z]*$')
521
522# cygwin sed fails with some expressions when they do not end with a single space.
523# see unit tests for details. Interestingly, the same expressions works perfectly
524# in cygwin shell.
525def utility_sed(name, args, interp, env, stdin, stdout, stderr, debugflags):
526 if 'debug-utility' in debugflags:
527 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
528
529 # Scan pattern arguments and append a space if necessary
530 for i in xrange(len(args)):
531 if not RE_SED.search(args[i]):
532 continue
533 args[i] = args[i] + ' '
534
535 return run_command(name, args, interp, env, stdin, stdout,
536 stderr, debugflags)
537
538#-------------------------------------------------------------------------------
539# sleep utility
540#-------------------------------------------------------------------------------
541def utility_sleep(name, args, interp, env, stdin, stdout, stderr, debugflags):
542 if 'debug-utility' in debugflags:
543 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
544 time.sleep(int(args[0]))
545 return 0
546
547#-------------------------------------------------------------------------------
548# sort utility
549#-------------------------------------------------------------------------------
550OPT_SORT = NonExitingParser("sort - sort, merge, or sequence check text files")
551
552def utility_sort(name, args, interp, env, stdin, stdout, stderr, debugflags):
553
554 def sort(path):
555 if path == '-':
556 lines = stdin.readlines()
557 else:
558 try:
559 f = file(path)
560 try:
561 lines = f.readlines()
562 finally:
563 f.close()
564 except IOError as e:
565 stderr.write(str(e) + '\n')
566 return 1
567
568 if lines and lines[-1][-1]!='\n':
569 lines[-1] = lines[-1] + '\n'
570 return lines
571
572 if 'debug-utility' in debugflags:
573 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
574
575 option, args = OPT_SORT.parse_args(args)
576 alllines = []
577
578 if len(args)<=0:
579 args += ['-']
580
581 # Load all files lines
582 curdir = os.getcwd()
583 try:
584 os.chdir(env['PWD'])
585 for path in args:
586 alllines += sort(path)
587 finally:
588 os.chdir(curdir)
589
590 alllines.sort()
591 for line in alllines:
592 stdout.write(line)
593 return 0
594
595#-------------------------------------------------------------------------------
596# hg utility
597#-------------------------------------------------------------------------------
598
599hgcommands = [
600 'add',
601 'addremove',
602 'commit', 'ci',
603 'debugrename',
604 'debugwalk',
605 'falabala', # Dummy command used in a mercurial test
606 'incoming',
607 'locate',
608 'pull',
609 'push',
610 'qinit',
611 'remove', 'rm',
612 'rename', 'mv',
613 'revert',
614 'showconfig',
615 'status', 'st',
616 'strip',
617 ]
618
619def rewriteslashes(name, args):
620 # Several hg commands output file paths, rewrite the separators
621 if len(args) > 1 and name.lower().endswith('python') \
622 and args[0].endswith('hg'):
623 for cmd in hgcommands:
624 if cmd in args[1:]:
625 return True
626
627 # svn output contains many paths with OS specific separators.
628 # Normalize these to unix paths.
629 base = os.path.basename(name)
630 if base.startswith('svn'):
631 return True
632
633 return False
634
635def rewritehg(output):
636 if not output:
637 return output
638 # Rewrite os specific messages
639 output = output.replace(': The system cannot find the file specified',
640 ': No such file or directory')
641 output = re.sub(': Access is denied.*$', ': Permission denied', output)
642 output = output.replace(': No connection could be made because the target machine actively refused it',
643 ': Connection refused')
644 return output
645
646
647def run_command(name, args, interp, env, stdin, stdout,
648 stderr, debugflags):
649 # Execute the command
650 if 'debug-utility' in debugflags:
651 print interp.log(' '.join([name, str(args), interp['PWD']]) + '\n')
652
653 hgbin = interp.options().hgbinary
654 ishg = hgbin and ('hg' in name or args and 'hg' in args[0])
655 unixoutput = 'cygwin' in name or ishg
656
657 exec_env = env.get_variables()
658 try:
659 # BUG: comparing file descriptor is clearly not a reliable way to tell
660 # whether they point on the same underlying object. But in pysh limited
661 # scope this is usually right, we do not expect complicated redirections
662 # besides usual 2>&1.
663 # Still there is one case we have but cannot deal with is when stdout
664 # and stderr are redirected *by pysh caller*. This the reason for the
665 # --redirect pysh() option.
666 # Now, we want to know they are the same because we sometimes need to
667 # transform the command output, mostly remove CR-LF to ensure that
668 # command output is unix-like. Cygwin utilies are a special case because
669 # they explicitely set their output streams to binary mode, so we have
670 # nothing to do. For all others commands, we have to guess whether they
671 # are sending text data, in which case the transformation must be done.
672 # Again, the NUL character test is unreliable but should be enough for
673 # hg tests.
674 redirected = stdout.fileno()==stderr.fileno()
675 if not redirected:
676 p = subprocess.Popen([name] + args, cwd=env['PWD'], env=exec_env,
677 stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
678 else:
679 p = subprocess.Popen([name] + args, cwd=env['PWD'], env=exec_env,
680 stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
681 out, err = p.communicate()
682 except WindowsError as e:
683 raise UtilityError(str(e))
684
685 if not unixoutput:
686 def encode(s):
687 if '\0' in s:
688 return s
689 return s.replace('\r\n', '\n')
690 else:
691 encode = lambda s: s
692
693 if rewriteslashes(name, args):
694 encode1_ = encode
695 def encode(s):
696 s = encode1_(s)
697 s = s.replace('\\\\', '\\')
698 s = s.replace('\\', '/')
699 return s
700
701 if ishg:
702 encode2_ = encode
703 def encode(s):
704 return rewritehg(encode2_(s))
705
706 stdout.write(encode(out))
707 if not redirected:
708 stderr.write(encode(err))
709 return p.returncode
710
diff --git a/bitbake/lib/bb/pysh/interp.py b/bitbake/lib/bb/pysh/interp.py
new file mode 100644
index 0000000000..25d8c92ec4
--- /dev/null
+++ b/bitbake/lib/bb/pysh/interp.py
@@ -0,0 +1,1367 @@
1# interp.py - shell interpreter for pysh.
2#
3# Copyright 2007 Patrick Mezard
4#
5# This software may be used and distributed according to the terms
6# of the GNU General Public License, incorporated herein by reference.
7
8"""Implement the shell interpreter.
9
10Most references are made to "The Open Group Base Specifications Issue 6".
11<http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html>
12"""
13# TODO: document the fact input streams must implement fileno() so Popen will work correctly.
14# it requires non-stdin stream to be implemented as files. Still to be tested...
15# DOC: pathsep is used in PATH instead of ':'. Clearly, there are path syntax issues here.
16# TODO: stop command execution upon error.
17# TODO: sort out the filename/io_number mess. It should be possible to use filenames only.
18# TODO: review subshell implementation
19# TODO: test environment cloning for non-special builtins
20# TODO: set -x should not rebuild commands from tokens, assignments/redirections are lost
21# TODO: unit test for variable assignment
22# TODO: test error management wrt error type/utility type
23# TODO: test for binary output everywhere
24# BUG: debug-parsing does not pass log file to PLY. Maybe a PLY upgrade is necessary.
25import base64
26import cPickle as pickle
27import errno
28import glob
29import os
30import re
31import subprocess
32import sys
33import tempfile
34
35try:
36 s = set()
37 del s
38except NameError:
39 from Set import Set as set
40
41import builtin
42from sherrors import *
43import pyshlex
44import pyshyacc
45
46def mappend(func, *args, **kargs):
47 """Like map but assume func returns a list. Returned lists are merged into
48 a single one.
49 """
50 return reduce(lambda a,b: a+b, map(func, *args, **kargs), [])
51
52class FileWrapper:
53 """File object wrapper to ease debugging.
54
55 Allow mode checking and implement file duplication through a simple
56 reference counting scheme. Not sure the latter is really useful since
57 only real file descriptors can be used.
58 """
59 def __init__(self, mode, file, close=True):
60 if mode not in ('r', 'w', 'a'):
61 raise IOError('invalid mode: %s' % mode)
62 self._mode = mode
63 self._close = close
64 if isinstance(file, FileWrapper):
65 if file._refcount[0] <= 0:
66 raise IOError(0, 'Error')
67 self._refcount = file._refcount
68 self._refcount[0] += 1
69 self._file = file._file
70 else:
71 self._refcount = [1]
72 self._file = file
73
74 def dup(self):
75 return FileWrapper(self._mode, self, self._close)
76
77 def fileno(self):
78 """fileno() should be only necessary for input streams."""
79 return self._file.fileno()
80
81 def read(self, size=-1):
82 if self._mode!='r':
83 raise IOError(0, 'Error')
84 return self._file.read(size)
85
86 def readlines(self, *args, **kwargs):
87 return self._file.readlines(*args, **kwargs)
88
89 def write(self, s):
90 if self._mode not in ('w', 'a'):
91 raise IOError(0, 'Error')
92 return self._file.write(s)
93
94 def flush(self):
95 self._file.flush()
96
97 def close(self):
98 if not self._refcount:
99 return
100 assert self._refcount[0] > 0
101
102 self._refcount[0] -= 1
103 if self._refcount[0] == 0:
104 self._mode = 'c'
105 if self._close:
106 self._file.close()
107 self._refcount = None
108
109 def mode(self):
110 return self._mode
111
112 def __getattr__(self, name):
113 if name == 'name':
114 self.name = getattr(self._file, name)
115 return self.name
116 else:
117 raise AttributeError(name)
118
119 def __del__(self):
120 self.close()
121
122
123def win32_open_devnull(mode):
124 return open('NUL', mode)
125
126
127class Redirections:
128 """Stores open files and their mapping to pseudo-sh file descriptor.
129 """
130 # BUG: redirections are not handled correctly: 1>&3 2>&3 3>&4 does
131 # not make 1 to redirect to 4
132 def __init__(self, stdin=None, stdout=None, stderr=None):
133 self._descriptors = {}
134 if stdin is not None:
135 self._add_descriptor(0, stdin)
136 if stdout is not None:
137 self._add_descriptor(1, stdout)
138 if stderr is not None:
139 self._add_descriptor(2, stderr)
140
141 def add_here_document(self, interp, name, content, io_number=None):
142 if io_number is None:
143 io_number = 0
144
145 if name==pyshlex.unquote_wordtree(name):
146 content = interp.expand_here_document(('TOKEN', content))
147
148 # Write document content in a temporary file
149 tmp = tempfile.TemporaryFile()
150 try:
151 tmp.write(content)
152 tmp.flush()
153 tmp.seek(0)
154 self._add_descriptor(io_number, FileWrapper('r', tmp))
155 except:
156 tmp.close()
157 raise
158
159 def add(self, interp, op, filename, io_number=None):
160 if op not in ('<', '>', '>|', '>>', '>&'):
161 # TODO: add descriptor duplication and here_documents
162 raise RedirectionError('Unsupported redirection operator "%s"' % op)
163
164 if io_number is not None:
165 io_number = int(io_number)
166
167 if (op == '>&' and filename.isdigit()) or filename=='-':
168 # No expansion for file descriptors, quote them if you want a filename
169 fullname = filename
170 else:
171 if filename.startswith('/'):
172 # TODO: win32 kludge
173 if filename=='/dev/null':
174 fullname = 'NUL'
175 else:
176 # TODO: handle absolute pathnames, they are unlikely to exist on the
177 # current platform (win32 for instance).
178 raise NotImplementedError()
179 else:
180 fullname = interp.expand_redirection(('TOKEN', filename))
181 if not fullname:
182 raise RedirectionError('%s: ambiguous redirect' % filename)
183 # Build absolute path based on PWD
184 fullname = os.path.join(interp.get_env()['PWD'], fullname)
185
186 if op=='<':
187 return self._add_input_redirection(interp, fullname, io_number)
188 elif op in ('>', '>|'):
189 clobber = ('>|'==op)
190 return self._add_output_redirection(interp, fullname, io_number, clobber)
191 elif op=='>>':
192 return self._add_output_appending(interp, fullname, io_number)
193 elif op=='>&':
194 return self._dup_output_descriptor(fullname, io_number)
195
196 def close(self):
197 if self._descriptors is not None:
198 for desc in self._descriptors.itervalues():
199 desc.flush()
200 desc.close()
201 self._descriptors = None
202
203 def stdin(self):
204 return self._descriptors[0]
205
206 def stdout(self):
207 return self._descriptors[1]
208
209 def stderr(self):
210 return self._descriptors[2]
211
212 def clone(self):
213 clone = Redirections()
214 for desc, fileobj in self._descriptors.iteritems():
215 clone._descriptors[desc] = fileobj.dup()
216 return clone
217
218 def _add_output_redirection(self, interp, filename, io_number, clobber):
219 if io_number is None:
220 # io_number default to standard output
221 io_number = 1
222
223 if not clobber and interp.get_env().has_opt('-C') and os.path.isfile(filename):
224 # File already exist in no-clobber mode, bail out
225 raise RedirectionError('File "%s" already exists' % filename)
226
227 # Open and register
228 self._add_file_descriptor(io_number, filename, 'w')
229
230 def _add_output_appending(self, interp, filename, io_number):
231 if io_number is None:
232 io_number = 1
233 self._add_file_descriptor(io_number, filename, 'a')
234
235 def _add_input_redirection(self, interp, filename, io_number):
236 if io_number is None:
237 io_number = 0
238 self._add_file_descriptor(io_number, filename, 'r')
239
240 def _add_file_descriptor(self, io_number, filename, mode):
241 try:
242 if filename.startswith('/'):
243 if filename=='/dev/null':
244 f = win32_open_devnull(mode+'b')
245 else:
246 # TODO: handle absolute pathnames, they are unlikely to exist on the
247 # current platform (win32 for instance).
248 raise NotImplementedError('cannot open absolute path %s' % repr(filename))
249 else:
250 f = file(filename, mode+'b')
251 except IOError as e:
252 raise RedirectionError(str(e))
253
254 wrapper = None
255 try:
256 wrapper = FileWrapper(mode, f)
257 f = None
258 self._add_descriptor(io_number, wrapper)
259 except:
260 if f: f.close()
261 if wrapper: wrapper.close()
262 raise
263
264 def _dup_output_descriptor(self, source_fd, dest_fd):
265 if source_fd is None:
266 source_fd = 1
267 self._dup_file_descriptor(source_fd, dest_fd, 'w')
268
269 def _dup_file_descriptor(self, source_fd, dest_fd, mode):
270 source_fd = int(source_fd)
271 if source_fd not in self._descriptors:
272 raise RedirectionError('"%s" is not a valid file descriptor' % str(source_fd))
273 source = self._descriptors[source_fd]
274
275 if source.mode()!=mode:
276 raise RedirectionError('Descriptor %s cannot be duplicated in mode "%s"' % (str(source), mode))
277
278 if dest_fd=='-':
279 # Close the source descriptor
280 del self._descriptors[source_fd]
281 source.close()
282 else:
283 dest_fd = int(dest_fd)
284 if dest_fd not in self._descriptors:
285 raise RedirectionError('Cannot replace file descriptor %s' % str(dest_fd))
286
287 dest = self._descriptors[dest_fd]
288 if dest.mode()!=mode:
289 raise RedirectionError('Descriptor %s cannot be cannot be redirected in mode "%s"' % (str(dest), mode))
290
291 self._descriptors[dest_fd] = source.dup()
292 dest.close()
293
294 def _add_descriptor(self, io_number, file):
295 io_number = int(io_number)
296
297 if io_number in self._descriptors:
298 # Close the current descriptor
299 d = self._descriptors[io_number]
300 del self._descriptors[io_number]
301 d.close()
302
303 self._descriptors[io_number] = file
304
305 def __str__(self):
306 names = [('%d=%r' % (k, getattr(v, 'name', None))) for k,v
307 in self._descriptors.iteritems()]
308 names = ','.join(names)
309 return 'Redirections(%s)' % names
310
311 def __del__(self):
312 self.close()
313
314def cygwin_to_windows_path(path):
315 """Turn /cygdrive/c/foo into c:/foo, or return path if it
316 is not a cygwin path.
317 """
318 if not path.startswith('/cygdrive/'):
319 return path
320 path = path[len('/cygdrive/'):]
321 path = path[:1] + ':' + path[1:]
322 return path
323
324def win32_to_unix_path(path):
325 if path is not None:
326 path = path.replace('\\', '/')
327 return path
328
329_RE_SHEBANG = re.compile(r'^\#!\s?([^\s]+)(?:\s([^\s]+))?')
330_SHEBANG_CMDS = {
331 '/usr/bin/env': 'env',
332 '/bin/sh': 'pysh',
333 'python': 'python',
334}
335
336def resolve_shebang(path, ignoreshell=False):
337 """Return a list of arguments as shebang interpreter call or an empty list
338 if path does not refer to an executable script.
339 See <http://www.opengroup.org/austin/docs/austin_51r2.txt>.
340
341 ignoreshell - set to True to ignore sh shebangs. Return an empty list instead.
342 """
343 try:
344 f = file(path)
345 try:
346 # At most 80 characters in the first line
347 header = f.read(80).splitlines()[0]
348 finally:
349 f.close()
350
351 m = _RE_SHEBANG.search(header)
352 if not m:
353 return []
354 cmd, arg = m.group(1,2)
355 if os.path.isfile(cmd):
356 # Keep this one, the hg script for instance contains a weird windows
357 # shebang referencing the current python install.
358 cmdfile = os.path.basename(cmd).lower()
359 if cmdfile == 'python.exe':
360 cmd = 'python'
361 pass
362 elif cmd not in _SHEBANG_CMDS:
363 raise CommandNotFound('Unknown interpreter "%s" referenced in '\
364 'shebang' % header)
365 cmd = _SHEBANG_CMDS.get(cmd)
366 if cmd is None or (ignoreshell and cmd == 'pysh'):
367 return []
368 if arg is None:
369 return [cmd, win32_to_unix_path(path)]
370 return [cmd, arg, win32_to_unix_path(path)]
371 except IOError as e:
372 if e.errno!=errno.ENOENT and \
373 (e.errno!=errno.EPERM and not os.path.isdir(path)): # Opening a directory raises EPERM
374 raise
375 return []
376
377def win32_find_in_path(name, path):
378 if isinstance(path, str):
379 path = path.split(os.pathsep)
380
381 exts = os.environ.get('PATHEXT', '').lower().split(os.pathsep)
382 for p in path:
383 p_name = os.path.join(p, name)
384
385 prefix = resolve_shebang(p_name)
386 if prefix:
387 return prefix
388
389 for ext in exts:
390 p_name_ext = p_name + ext
391 if os.path.exists(p_name_ext):
392 return [win32_to_unix_path(p_name_ext)]
393 return []
394
395class Traps(dict):
396 def __setitem__(self, key, value):
397 if key not in ('EXIT',):
398 raise NotImplementedError()
399 super(Traps, self).__setitem__(key, value)
400
401# IFS white spaces character class
402_IFS_WHITESPACES = (' ', '\t', '\n')
403
404class Environment:
405 """Environment holds environment variables, export table, function
406 definitions and whatever is defined in 2.12 "Shell Execution Environment",
407 redirection excepted.
408 """
409 def __init__(self, pwd):
410 self._opt = set() #Shell options
411
412 self._functions = {}
413 self._env = {'?': '0', '#': '0'}
414 self._exported = set([
415 'HOME', 'IFS', 'PATH'
416 ])
417
418 # Set environment vars with side-effects
419 self._ifs_ws = None # Set of IFS whitespace characters
420 self._ifs_re = None # Regular expression used to split between words using IFS classes
421 self['IFS'] = ''.join(_IFS_WHITESPACES) #Default environment values
422 self['PWD'] = pwd
423 self.traps = Traps()
424
425 def clone(self, subshell=False):
426 env = Environment(self['PWD'])
427 env._opt = set(self._opt)
428 for k,v in self.get_variables().iteritems():
429 if k in self._exported:
430 env.export(k,v)
431 elif subshell:
432 env[k] = v
433
434 if subshell:
435 env._functions = dict(self._functions)
436
437 return env
438
439 def __getitem__(self, key):
440 if key in ('@', '*', '-', '$'):
441 raise NotImplementedError('%s is not implemented' % repr(key))
442 return self._env[key]
443
444 def get(self, key, defval=None):
445 try:
446 return self[key]
447 except KeyError:
448 return defval
449
450 def __setitem__(self, key, value):
451 if key=='IFS':
452 # Update the whitespace/non-whitespace classes
453 self._update_ifs(value)
454 elif key=='PWD':
455 pwd = os.path.abspath(value)
456 if not os.path.isdir(pwd):
457 raise VarAssignmentError('Invalid directory %s' % value)
458 value = pwd
459 elif key in ('?', '!'):
460 value = str(int(value))
461 self._env[key] = value
462
463 def __delitem__(self, key):
464 if key in ('IFS', 'PWD', '?'):
465 raise VarAssignmentError('%s cannot be unset' % key)
466 del self._env[key]
467
468 def __contains__(self, item):
469 return item in self._env
470
471 def set_positional_args(self, args):
472 """Set the content of 'args' as positional argument from 1 to len(args).
473 Return previous argument as a list of strings.
474 """
475 # Save and remove previous arguments
476 prevargs = []
477 for i in xrange(int(self._env['#'])):
478 i = str(i+1)
479 prevargs.append(self._env[i])
480 del self._env[i]
481 self._env['#'] = '0'
482
483 #Set new ones
484 for i,arg in enumerate(args):
485 self._env[str(i+1)] = str(arg)
486 self._env['#'] = str(len(args))
487
488 return prevargs
489
490 def get_positional_args(self):
491 return [self._env[str(i+1)] for i in xrange(int(self._env['#']))]
492
493 def get_variables(self):
494 return dict(self._env)
495
496 def export(self, key, value=None):
497 if value is not None:
498 self[key] = value
499 self._exported.add(key)
500
501 def get_exported(self):
502 return [(k,self._env.get(k)) for k in self._exported]
503
504 def split_fields(self, word):
505 if not self._ifs_ws or not word:
506 return [word]
507 return re.split(self._ifs_re, word)
508
509 def _update_ifs(self, value):
510 """Update the split_fields related variables when IFS character set is
511 changed.
512 """
513 # TODO: handle NULL IFS
514
515 # Separate characters in whitespace and non-whitespace
516 chars = set(value)
517 ws = [c for c in chars if c in _IFS_WHITESPACES]
518 nws = [c for c in chars if c not in _IFS_WHITESPACES]
519
520 # Keep whitespaces in a string for left and right stripping
521 self._ifs_ws = ''.join(ws)
522
523 # Build a regexp to split fields
524 trailing = '[' + ''.join([re.escape(c) for c in ws]) + ']'
525 if nws:
526 # First, the single non-whitespace occurence.
527 nws = '[' + ''.join([re.escape(c) for c in nws]) + ']'
528 nws = '(?:' + trailing + '*' + nws + trailing + '*' + '|' + trailing + '+)'
529 else:
530 # Then mix all parts with quantifiers
531 nws = trailing + '+'
532 self._ifs_re = re.compile(nws)
533
534 def has_opt(self, opt, val=None):
535 return (opt, val) in self._opt
536
537 def set_opt(self, opt, val=None):
538 self._opt.add((opt, val))
539
540 def find_in_path(self, name, pwd=False):
541 path = self._env.get('PATH', '').split(os.pathsep)
542 if pwd:
543 path[:0] = [self['PWD']]
544 if os.name == 'nt':
545 return win32_find_in_path(name, self._env.get('PATH', ''))
546 else:
547 raise NotImplementedError()
548
549 def define_function(self, name, body):
550 if not is_name(name):
551 raise ShellSyntaxError('%s is not a valid function name' % repr(name))
552 self._functions[name] = body
553
554 def remove_function(self, name):
555 del self._functions[name]
556
557 def is_function(self, name):
558 return name in self._functions
559
560 def get_function(self, name):
561 return self._functions.get(name)
562
563
564name_charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
565name_charset = dict(zip(name_charset,name_charset))
566
567def match_name(s):
568 """Return the length in characters of the longest prefix made of name
569 allowed characters in s.
570 """
571 for i,c in enumerate(s):
572 if c not in name_charset:
573 return s[:i]
574 return s
575
576def is_name(s):
577 return len([c for c in s if c not in name_charset])<=0
578
579def is_special_param(c):
580 return len(c)==1 and c in ('@','*','#','?','-','$','!','0')
581
582def utility_not_implemented(name, *args, **kwargs):
583 raise NotImplementedError('%s utility is not implemented' % name)
584
585
586class Utility:
587 """Define utilities properties:
588 func -- utility callable. See builtin module for utility samples.
589 is_special -- see XCU 2.8.
590 """
591 def __init__(self, func, is_special=0):
592 self.func = func
593 self.is_special = bool(is_special)
594
595
596def encodeargs(args):
597 def encodearg(s):
598 lines = base64.encodestring(s)
599 lines = [l.splitlines()[0] for l in lines]
600 return ''.join(lines)
601
602 s = pickle.dumps(args)
603 return encodearg(s)
604
605def decodeargs(s):
606 s = base64.decodestring(s)
607 return pickle.loads(s)
608
609
610class GlobError(Exception):
611 pass
612
613class Options:
614 def __init__(self):
615 # True if Mercurial operates with binary streams
616 self.hgbinary = True
617
618class Interpreter:
619 # Implementation is very basic: the execute() method just makes a DFS on the
620 # AST and execute nodes one by one. Nodes are tuple (name,obj) where name
621 # is a string identifier and obj the AST element returned by the parser.
622 #
623 # Handler are named after the node identifiers.
624 # TODO: check node names and remove the switch in execute with some
625 # dynamic getattr() call to find node handlers.
626 """Shell interpreter.
627
628 The following debugging flags can be passed:
629 debug-parsing - enable PLY debugging.
630 debug-tree - print the generated AST.
631 debug-cmd - trace command execution before word expansion, plus exit status.
632 debug-utility - trace utility execution.
633 """
634
635 # List supported commands.
636 COMMANDS = {
637 'cat': Utility(builtin.utility_cat,),
638 'cd': Utility(builtin.utility_cd,),
639 ':': Utility(builtin.utility_colon,),
640 'echo': Utility(builtin.utility_echo),
641 'env': Utility(builtin.utility_env),
642 'exit': Utility(builtin.utility_exit),
643 'export': Utility(builtin.builtin_export, is_special=1),
644 'egrep': Utility(builtin.utility_egrep),
645 'fgrep': Utility(builtin.utility_fgrep),
646 'gunzip': Utility(builtin.utility_gunzip),
647 'kill': Utility(builtin.utility_kill),
648 'mkdir': Utility(builtin.utility_mkdir),
649 'netstat': Utility(builtin.utility_netstat),
650 'printf': Utility(builtin.utility_printf),
651 'pwd': Utility(builtin.utility_pwd),
652 'return': Utility(builtin.builtin_return, is_special=1),
653 'sed': Utility(builtin.utility_sed,),
654 'set': Utility(builtin.builtin_set,),
655 'shift': Utility(builtin.builtin_shift,),
656 'sleep': Utility(builtin.utility_sleep,),
657 'sort': Utility(builtin.utility_sort,),
658 'trap': Utility(builtin.builtin_trap, is_special=1),
659 'true': Utility(builtin.utility_true),
660 'unset': Utility(builtin.builtin_unset, is_special=1),
661 'wait': Utility(builtin.builtin_wait, is_special=1),
662 }
663
664 def __init__(self, pwd, debugflags = [], env=None, redirs=None, stdin=None,
665 stdout=None, stderr=None, opts=Options()):
666 self._env = env
667 if self._env is None:
668 self._env = Environment(pwd)
669 self._children = {}
670
671 self._redirs = redirs
672 self._close_redirs = False
673
674 if self._redirs is None:
675 if stdin is None:
676 stdin = sys.stdin
677 if stdout is None:
678 stdout = sys.stdout
679 if stderr is None:
680 stderr = sys.stderr
681 stdin = FileWrapper('r', stdin, False)
682 stdout = FileWrapper('w', stdout, False)
683 stderr = FileWrapper('w', stderr, False)
684 self._redirs = Redirections(stdin, stdout, stderr)
685 self._close_redirs = True
686
687 self._debugflags = list(debugflags)
688 self._logfile = sys.stderr
689 self._options = opts
690
691 def close(self):
692 """Must be called when the interpreter is no longer used."""
693 script = self._env.traps.get('EXIT')
694 if script:
695 try:
696 self.execute_script(script=script)
697 except:
698 pass
699
700 if self._redirs is not None and self._close_redirs:
701 self._redirs.close()
702 self._redirs = None
703
704 def log(self, s):
705 self._logfile.write(s)
706 self._logfile.flush()
707
708 def __getitem__(self, key):
709 return self._env[key]
710
711 def __setitem__(self, key, value):
712 self._env[key] = value
713
714 def options(self):
715 return self._options
716
717 def redirect(self, redirs, ios):
718 def add_redir(io):
719 if isinstance(io, pyshyacc.IORedirect):
720 redirs.add(self, io.op, io.filename, io.io_number)
721 else:
722 redirs.add_here_document(self, io.name, io.content, io.io_number)
723
724 map(add_redir, ios)
725 return redirs
726
727 def execute_script(self, script=None, ast=None, sourced=False,
728 scriptpath=None):
729 """If script is not None, parse the input. Otherwise takes the supplied
730 AST. Then execute the AST.
731 Return the script exit status.
732 """
733 try:
734 if scriptpath is not None:
735 self._env['0'] = os.path.abspath(scriptpath)
736
737 if script is not None:
738 debug_parsing = ('debug-parsing' in self._debugflags)
739 cmds, script = pyshyacc.parse(script, True, debug_parsing)
740 if 'debug-tree' in self._debugflags:
741 pyshyacc.print_commands(cmds, self._logfile)
742 self._logfile.flush()
743 else:
744 cmds, script = ast, ''
745
746 status = 0
747 for cmd in cmds:
748 try:
749 status = self.execute(cmd)
750 except ExitSignal as e:
751 if sourced:
752 raise
753 status = int(e.args[0])
754 return status
755 except ShellError:
756 self._env['?'] = 1
757 raise
758 if 'debug-utility' in self._debugflags or 'debug-cmd' in self._debugflags:
759 self.log('returncode ' + str(status)+ '\n')
760 return status
761 except CommandNotFound as e:
762 print >>self._redirs.stderr, str(e)
763 self._redirs.stderr.flush()
764 # Command not found by non-interactive shell
765 # return 127
766 raise
767 except RedirectionError as e:
768 # TODO: should be handled depending on the utility status
769 print >>self._redirs.stderr, str(e)
770 self._redirs.stderr.flush()
771 # Command not found by non-interactive shell
772 # return 127
773 raise
774
775 def dotcommand(self, env, args):
776 if len(args) < 1:
777 raise ShellError('. expects at least one argument')
778 path = args[0]
779 if '/' not in path:
780 found = env.find_in_path(args[0], True)
781 if found:
782 path = found[0]
783 script = file(path).read()
784 return self.execute_script(script=script, sourced=True)
785
786 def execute(self, token, redirs=None):
787 """Execute and AST subtree with supplied redirections overriding default
788 interpreter ones.
789 Return the exit status.
790 """
791 if not token:
792 return 0
793
794 if redirs is None:
795 redirs = self._redirs
796
797 if isinstance(token, list):
798 # Commands sequence
799 res = 0
800 for t in token:
801 res = self.execute(t, redirs)
802 return res
803
804 type, value = token
805 status = 0
806 if type=='simple_command':
807 redirs_copy = redirs.clone()
808 try:
809 # TODO: define and handle command return values
810 # TODO: implement set -e
811 status = self._execute_simple_command(value, redirs_copy)
812 finally:
813 redirs_copy.close()
814 elif type=='pipeline':
815 status = self._execute_pipeline(value, redirs)
816 elif type=='and_or':
817 status = self._execute_and_or(value, redirs)
818 elif type=='for_clause':
819 status = self._execute_for_clause(value, redirs)
820 elif type=='while_clause':
821 status = self._execute_while_clause(value, redirs)
822 elif type=='function_definition':
823 status = self._execute_function_definition(value, redirs)
824 elif type=='brace_group':
825 status = self._execute_brace_group(value, redirs)
826 elif type=='if_clause':
827 status = self._execute_if_clause(value, redirs)
828 elif type=='subshell':
829 status = self.subshell(ast=value.cmds, redirs=redirs)
830 elif type=='async':
831 status = self._asynclist(value)
832 elif type=='redirect_list':
833 redirs_copy = self.redirect(redirs.clone(), value.redirs)
834 try:
835 status = self.execute(value.cmd, redirs_copy)
836 finally:
837 redirs_copy.close()
838 else:
839 raise NotImplementedError('Unsupported token type ' + type)
840
841 if status < 0:
842 status = 255
843 return status
844
845 def _execute_if_clause(self, if_clause, redirs):
846 cond_status = self.execute(if_clause.cond, redirs)
847 if cond_status==0:
848 return self.execute(if_clause.if_cmds, redirs)
849 else:
850 return self.execute(if_clause.else_cmds, redirs)
851
852 def _execute_brace_group(self, group, redirs):
853 status = 0
854 for cmd in group.cmds:
855 status = self.execute(cmd, redirs)
856 return status
857
858 def _execute_function_definition(self, fundef, redirs):
859 self._env.define_function(fundef.name, fundef.body)
860 return 0
861
862 def _execute_while_clause(self, while_clause, redirs):
863 status = 0
864 while 1:
865 cond_status = 0
866 for cond in while_clause.condition:
867 cond_status = self.execute(cond, redirs)
868
869 if cond_status:
870 break
871
872 for cmd in while_clause.cmds:
873 status = self.execute(cmd, redirs)
874
875 return status
876
877 def _execute_for_clause(self, for_clause, redirs):
878 if not is_name(for_clause.name):
879 raise ShellSyntaxError('%s is not a valid name' % repr(for_clause.name))
880 items = mappend(self.expand_token, for_clause.items)
881
882 status = 0
883 for item in items:
884 self._env[for_clause.name] = item
885 for cmd in for_clause.cmds:
886 status = self.execute(cmd, redirs)
887 return status
888
889 def _execute_and_or(self, or_and, redirs):
890 res = self.execute(or_and.left, redirs)
891 if (or_and.op=='&&' and res==0) or (or_and.op!='&&' and res!=0):
892 res = self.execute(or_and.right, redirs)
893 return res
894
895 def _execute_pipeline(self, pipeline, redirs):
896 if len(pipeline.commands)==1:
897 status = self.execute(pipeline.commands[0], redirs)
898 else:
899 # Execute all commands one after the other
900 status = 0
901 inpath, outpath = None, None
902 try:
903 # Commands inputs and outputs cannot really be plugged as done
904 # by a real shell. Run commands sequentially and chain their
905 # input/output throught temporary files.
906 tmpfd, inpath = tempfile.mkstemp()
907 os.close(tmpfd)
908 tmpfd, outpath = tempfile.mkstemp()
909 os.close(tmpfd)
910
911 inpath = win32_to_unix_path(inpath)
912 outpath = win32_to_unix_path(outpath)
913
914 for i, cmd in enumerate(pipeline.commands):
915 call_redirs = redirs.clone()
916 try:
917 if i!=0:
918 call_redirs.add(self, '<', inpath)
919 if i!=len(pipeline.commands)-1:
920 call_redirs.add(self, '>', outpath)
921
922 status = self.execute(cmd, call_redirs)
923
924 # Chain inputs/outputs
925 inpath, outpath = outpath, inpath
926 finally:
927 call_redirs.close()
928 finally:
929 if inpath: os.remove(inpath)
930 if outpath: os.remove(outpath)
931
932 if pipeline.reverse_status:
933 status = int(not status)
934 self._env['?'] = status
935 return status
936
937 def _execute_function(self, name, args, interp, env, stdin, stdout, stderr, *others):
938 assert interp is self
939
940 func = env.get_function(name)
941 #Set positional parameters
942 prevargs = None
943 try:
944 prevargs = env.set_positional_args(args)
945 try:
946 redirs = Redirections(stdin.dup(), stdout.dup(), stderr.dup())
947 try:
948 status = self.execute(func, redirs)
949 finally:
950 redirs.close()
951 except ReturnSignal as e:
952 status = int(e.args[0])
953 env['?'] = status
954 return status
955 finally:
956 #Reset positional parameters
957 if prevargs is not None:
958 env.set_positional_args(prevargs)
959
960 def _execute_simple_command(self, token, redirs):
961 """Can raise ReturnSignal when return builtin is called, ExitSignal when
962 exit is called, and other shell exceptions upon builtin failures.
963 """
964 debug_command = 'debug-cmd' in self._debugflags
965 if debug_command:
966 self.log('word' + repr(token.words) + '\n')
967 self.log('assigns' + repr(token.assigns) + '\n')
968 self.log('redirs' + repr(token.redirs) + '\n')
969
970 is_special = None
971 env = self._env
972
973 try:
974 # Word expansion
975 args = []
976 for word in token.words:
977 args += self.expand_token(word)
978 if is_special is None and args:
979 is_special = env.is_function(args[0]) or \
980 (args[0] in self.COMMANDS and self.COMMANDS[args[0]].is_special)
981
982 if debug_command:
983 self.log('_execute_simple_command' + str(args) + '\n')
984
985 if not args:
986 # Redirections happen is a subshell
987 redirs = redirs.clone()
988 elif not is_special:
989 env = self._env.clone()
990
991 # Redirections
992 self.redirect(redirs, token.redirs)
993
994 # Variables assignments
995 res = 0
996 for type,(k,v) in token.assigns:
997 status, expanded = self.expand_variable((k,v))
998 if status is not None:
999 res = status
1000 if args:
1001 env.export(k, expanded)
1002 else:
1003 env[k] = expanded
1004
1005 if args and args[0] in ('.', 'source'):
1006 res = self.dotcommand(env, args[1:])
1007 elif args:
1008 if args[0] in self.COMMANDS:
1009 command = self.COMMANDS[args[0]]
1010 elif env.is_function(args[0]):
1011 command = Utility(self._execute_function, is_special=True)
1012 else:
1013 if not '/' in args[0].replace('\\', '/'):
1014 cmd = env.find_in_path(args[0])
1015 if not cmd:
1016 # TODO: test error code on unknown command => 127
1017 raise CommandNotFound('Unknown command: "%s"' % args[0])
1018 else:
1019 # Handle commands like '/cygdrive/c/foo.bat'
1020 cmd = cygwin_to_windows_path(args[0])
1021 if not os.path.exists(cmd):
1022 raise CommandNotFound('%s: No such file or directory' % args[0])
1023 shebang = resolve_shebang(cmd)
1024 if shebang:
1025 cmd = shebang
1026 else:
1027 cmd = [cmd]
1028 args[0:1] = cmd
1029 command = Utility(builtin.run_command)
1030
1031 # Command execution
1032 if 'debug-cmd' in self._debugflags:
1033 self.log('redirections ' + str(redirs) + '\n')
1034
1035 res = command.func(args[0], args[1:], self, env,
1036 redirs.stdin(), redirs.stdout(),
1037 redirs.stderr(), self._debugflags)
1038
1039 if self._env.has_opt('-x'):
1040 # Trace command execution in shell environment
1041 # BUG: would be hard to reproduce a real shell behaviour since
1042 # the AST is not annotated with source lines/tokens.
1043 self._redirs.stdout().write(' '.join(args))
1044
1045 except ReturnSignal:
1046 raise
1047 except ShellError as e:
1048 if is_special or isinstance(e, (ExitSignal,
1049 ShellSyntaxError, ExpansionError)):
1050 raise e
1051 self._redirs.stderr().write(str(e)+'\n')
1052 return 1
1053
1054 return res
1055
1056 def expand_token(self, word):
1057 """Expand a word as specified in [2.6 Word Expansions]. Return the list
1058 of expanded words.
1059 """
1060 status, wtrees = self._expand_word(word)
1061 return map(pyshlex.wordtree_as_string, wtrees)
1062
1063 def expand_variable(self, word):
1064 """Return a status code (or None if no command expansion occurred)
1065 and a single word.
1066 """
1067 status, wtrees = self._expand_word(word, pathname=False, split=False)
1068 words = map(pyshlex.wordtree_as_string, wtrees)
1069 assert len(words)==1
1070 return status, words[0]
1071
1072 def expand_here_document(self, word):
1073 """Return the expanded document as a single word. The here document is
1074 assumed to be unquoted.
1075 """
1076 status, wtrees = self._expand_word(word, pathname=False,
1077 split=False, here_document=True)
1078 words = map(pyshlex.wordtree_as_string, wtrees)
1079 assert len(words)==1
1080 return words[0]
1081
1082 def expand_redirection(self, word):
1083 """Return a single word."""
1084 return self.expand_variable(word)[1]
1085
1086 def get_env(self):
1087 return self._env
1088
1089 def _expand_word(self, token, pathname=True, split=True, here_document=False):
1090 wtree = pyshlex.make_wordtree(token[1], here_document=here_document)
1091
1092 # TODO: implement tilde expansion
1093 def expand(wtree):
1094 """Return a pseudo wordtree: the tree or its subelements can be empty
1095 lists when no value result from the expansion.
1096 """
1097 status = None
1098 for part in wtree:
1099 if not isinstance(part, list):
1100 continue
1101 if part[0]in ("'", '\\'):
1102 continue
1103 elif part[0] in ('`', '$('):
1104 status, result = self._expand_command(part)
1105 part[:] = result
1106 elif part[0] in ('$', '${'):
1107 part[:] = self._expand_parameter(part, wtree[0]=='"', split)
1108 elif part[0] in ('', '"'):
1109 status, result = expand(part)
1110 part[:] = result
1111 else:
1112 raise NotImplementedError('%s expansion is not implemented'
1113 % part[0])
1114 # [] is returned when an expansion result in no-field,
1115 # like an empty $@
1116 wtree = [p for p in wtree if p != []]
1117 if len(wtree) < 3:
1118 return status, []
1119 return status, wtree
1120
1121 status, wtree = expand(wtree)
1122 if len(wtree) == 0:
1123 return status, wtree
1124 wtree = pyshlex.normalize_wordtree(wtree)
1125
1126 if split:
1127 wtrees = self._split_fields(wtree)
1128 else:
1129 wtrees = [wtree]
1130
1131 if pathname:
1132 wtrees = mappend(self._expand_pathname, wtrees)
1133
1134 wtrees = map(self._remove_quotes, wtrees)
1135 return status, wtrees
1136
1137 def _expand_command(self, wtree):
1138 # BUG: there is something to do with backslashes and quoted
1139 # characters here
1140 command = pyshlex.wordtree_as_string(wtree[1:-1])
1141 status, output = self.subshell_output(command)
1142 return status, ['', output, '']
1143
1144 def _expand_parameter(self, wtree, quoted=False, split=False):
1145 """Return a valid wtree or an empty list when no parameter results."""
1146 # Get the parameter name
1147 # TODO: implement weird expansion rules with ':'
1148 name = pyshlex.wordtree_as_string(wtree[1:-1])
1149 if not is_name(name) and not is_special_param(name):
1150 raise ExpansionError('Bad substitution "%s"' % name)
1151 # TODO: implement special parameters
1152 if name in ('@', '*'):
1153 args = self._env.get_positional_args()
1154 if len(args) == 0:
1155 return []
1156 if len(args)<2:
1157 return ['', ''.join(args), '']
1158
1159 sep = self._env.get('IFS', '')[:1]
1160 if split and quoted and name=='@':
1161 # Introduce a new token to tell the caller that these parameters
1162 # cause a split as specified in 2.5.2
1163 return ['@'] + args + ['']
1164 else:
1165 return ['', sep.join(args), '']
1166
1167 return ['', self._env.get(name, ''), '']
1168
1169 def _split_fields(self, wtree):
1170 def is_empty(split):
1171 return split==['', '', '']
1172
1173 def split_positional(quoted):
1174 # Return a list of wtree split according positional parameters rules.
1175 # All remaining '@' groups are removed.
1176 assert quoted[0]=='"'
1177
1178 splits = [[]]
1179 for part in quoted:
1180 if not isinstance(part, list) or part[0]!='@':
1181 splits[-1].append(part)
1182 else:
1183 # Empty or single argument list were dealt with already
1184 assert len(part)>3
1185 # First argument must join with the beginning part of the original word
1186 splits[-1].append(part[1])
1187 # Create double-quotes expressions for every argument after the first
1188 for arg in part[2:-1]:
1189 splits[-1].append('"')
1190 splits.append(['"', arg])
1191 return splits
1192
1193 # At this point, all expansions but pathnames have occured. Only quoted
1194 # and positional sequences remain. Thus, all candidates for field splitting
1195 # are in the tree root, or are positional splits ('@') and lie in root
1196 # children.
1197 if not wtree or wtree[0] not in ('', '"'):
1198 # The whole token is quoted or empty, nothing to split
1199 return [wtree]
1200
1201 if wtree[0]=='"':
1202 wtree = ['', wtree, '']
1203
1204 result = [['', '']]
1205 for part in wtree[1:-1]:
1206 if isinstance(part, list):
1207 if part[0]=='"':
1208 splits = split_positional(part)
1209 if len(splits)<=1:
1210 result[-1] += [part, '']
1211 else:
1212 # Terminate the current split
1213 result[-1] += [splits[0], '']
1214 result += splits[1:-1]
1215 # Create a new split
1216 result += [['', splits[-1], '']]
1217 else:
1218 result[-1] += [part, '']
1219 else:
1220 splits = self._env.split_fields(part)
1221 if len(splits)<=1:
1222 # No split
1223 result[-1][-1] += part
1224 else:
1225 # Terminate the current resulting part and create a new one
1226 result[-1][-1] += splits[0]
1227 result[-1].append('')
1228 result += [['', r, ''] for r in splits[1:-1]]
1229 result += [['', splits[-1]]]
1230 result[-1].append('')
1231
1232 # Leading and trailing empty groups come from leading/trailing blanks
1233 if result and is_empty(result[-1]):
1234 result[-1:] = []
1235 if result and is_empty(result[0]):
1236 result[:1] = []
1237 return result
1238
1239 def _expand_pathname(self, wtree):
1240 """See [2.6.6 Pathname Expansion]."""
1241 if self._env.has_opt('-f'):
1242 return [wtree]
1243
1244 # All expansions have been performed, only quoted sequences should remain
1245 # in the tree. Generate the pattern by folding the tree, escaping special
1246 # characters when appear quoted
1247 special_chars = '*?[]'
1248
1249 def make_pattern(wtree):
1250 subpattern = []
1251 for part in wtree[1:-1]:
1252 if isinstance(part, list):
1253 part = make_pattern(part)
1254 elif wtree[0]!='':
1255 for c in part:
1256 # Meta-characters cannot be quoted
1257 if c in special_chars:
1258 raise GlobError()
1259 subpattern.append(part)
1260 return ''.join(subpattern)
1261
1262 def pwd_glob(pattern):
1263 cwd = os.getcwd()
1264 os.chdir(self._env['PWD'])
1265 try:
1266 return glob.glob(pattern)
1267 finally:
1268 os.chdir(cwd)
1269
1270 #TODO: check working directory issues here wrt relative patterns
1271 try:
1272 pattern = make_pattern(wtree)
1273 paths = pwd_glob(pattern)
1274 except GlobError:
1275 # BUG: Meta-characters were found in quoted sequences. The should
1276 # have been used literally but this is unsupported in current glob module.
1277 # Instead we consider the whole tree must be used literally and
1278 # therefore there is no point in globbing. This is wrong when meta
1279 # characters are mixed with quoted meta in the same pattern like:
1280 # < foo*"py*" >
1281 paths = []
1282
1283 if not paths:
1284 return [wtree]
1285 return [['', path, ''] for path in paths]
1286
1287 def _remove_quotes(self, wtree):
1288 """See [2.6.7 Quote Removal]."""
1289
1290 def unquote(wtree):
1291 unquoted = []
1292 for part in wtree[1:-1]:
1293 if isinstance(part, list):
1294 part = unquote(part)
1295 unquoted.append(part)
1296 return ''.join(unquoted)
1297
1298 return ['', unquote(wtree), '']
1299
1300 def subshell(self, script=None, ast=None, redirs=None):
1301 """Execute the script or AST in a subshell, with inherited redirections
1302 if redirs is not None.
1303 """
1304 if redirs:
1305 sub_redirs = redirs
1306 else:
1307 sub_redirs = redirs.clone()
1308
1309 subshell = None
1310 try:
1311 subshell = Interpreter(None, self._debugflags, self._env.clone(True),
1312 sub_redirs, opts=self._options)
1313 return subshell.execute_script(script, ast)
1314 finally:
1315 if not redirs: sub_redirs.close()
1316 if subshell: subshell.close()
1317
1318 def subshell_output(self, script):
1319 """Execute the script in a subshell and return the captured output."""
1320 # Create temporary file to capture subshell output
1321 tmpfd, tmppath = tempfile.mkstemp()
1322 try:
1323 tmpfile = os.fdopen(tmpfd, 'wb')
1324 stdout = FileWrapper('w', tmpfile)
1325
1326 redirs = Redirections(self._redirs.stdin().dup(),
1327 stdout,
1328 self._redirs.stderr().dup())
1329 try:
1330 status = self.subshell(script=script, redirs=redirs)
1331 finally:
1332 redirs.close()
1333 redirs = None
1334
1335 # Extract subshell standard output
1336 tmpfile = open(tmppath, 'rb')
1337 try:
1338 output = tmpfile.read()
1339 return status, output.rstrip('\n')
1340 finally:
1341 tmpfile.close()
1342 finally:
1343 os.remove(tmppath)
1344
1345 def _asynclist(self, cmd):
1346 args = (self._env.get_variables(), cmd)
1347 arg = encodeargs(args)
1348 assert len(args) < 30*1024
1349 cmd = ['pysh.bat', '--ast', '-c', arg]
1350 p = subprocess.Popen(cmd, cwd=self._env['PWD'])
1351 self._children[p.pid] = p
1352 self._env['!'] = p.pid
1353 return 0
1354
1355 def wait(self, pids=None):
1356 if not pids:
1357 pids = self._children.keys()
1358
1359 status = 127
1360 for pid in pids:
1361 if pid not in self._children:
1362 continue
1363 p = self._children.pop(pid)
1364 status = p.wait()
1365
1366 return status
1367
diff --git a/bitbake/lib/bb/pysh/lsprof.py b/bitbake/lib/bb/pysh/lsprof.py
new file mode 100644
index 0000000000..b1831c22a7
--- /dev/null
+++ b/bitbake/lib/bb/pysh/lsprof.py
@@ -0,0 +1,116 @@
1#! /usr/bin/env python
2
3import sys
4from _lsprof import Profiler, profiler_entry
5
6__all__ = ['profile', 'Stats']
7
8def profile(f, *args, **kwds):
9 """XXX docstring"""
10 p = Profiler()
11 p.enable(subcalls=True, builtins=True)
12 try:
13 f(*args, **kwds)
14 finally:
15 p.disable()
16 return Stats(p.getstats())
17
18
19class Stats(object):
20 """XXX docstring"""
21
22 def __init__(self, data):
23 self.data = data
24
25 def sort(self, crit="inlinetime"):
26 """XXX docstring"""
27 if crit not in profiler_entry.__dict__:
28 raise ValueError("Can't sort by %s" % crit)
29 self.data.sort(lambda b, a: cmp(getattr(a, crit),
30 getattr(b, crit)))
31 for e in self.data:
32 if e.calls:
33 e.calls.sort(lambda b, a: cmp(getattr(a, crit),
34 getattr(b, crit)))
35
36 def pprint(self, top=None, file=None, limit=None, climit=None):
37 """XXX docstring"""
38 if file is None:
39 file = sys.stdout
40 d = self.data
41 if top is not None:
42 d = d[:top]
43 cols = "% 12s %12s %11.4f %11.4f %s\n"
44 hcols = "% 12s %12s %12s %12s %s\n"
45 cols2 = "+%12s %12s %11.4f %11.4f + %s\n"
46 file.write(hcols % ("CallCount", "Recursive", "Total(ms)",
47 "Inline(ms)", "module:lineno(function)"))
48 count = 0
49 for e in d:
50 file.write(cols % (e.callcount, e.reccallcount, e.totaltime,
51 e.inlinetime, label(e.code)))
52 count += 1
53 if limit is not None and count == limit:
54 return
55 ccount = 0
56 if e.calls:
57 for se in e.calls:
58 file.write(cols % ("+%s" % se.callcount, se.reccallcount,
59 se.totaltime, se.inlinetime,
60 "+%s" % label(se.code)))
61 count += 1
62 ccount += 1
63 if limit is not None and count == limit:
64 return
65 if climit is not None and ccount == climit:
66 break
67
68 def freeze(self):
69 """Replace all references to code objects with string
70 descriptions; this makes it possible to pickle the instance."""
71
72 # this code is probably rather ickier than it needs to be!
73 for i in range(len(self.data)):
74 e = self.data[i]
75 if not isinstance(e.code, str):
76 self.data[i] = type(e)((label(e.code),) + e[1:])
77 if e.calls:
78 for j in range(len(e.calls)):
79 se = e.calls[j]
80 if not isinstance(se.code, str):
81 e.calls[j] = type(se)((label(se.code),) + se[1:])
82
83_fn2mod = {}
84
85def label(code):
86 if isinstance(code, str):
87 return code
88 try:
89 mname = _fn2mod[code.co_filename]
90 except KeyError:
91 for k, v in sys.modules.items():
92 if v is None:
93 continue
94 if not hasattr(v, '__file__'):
95 continue
96 if not isinstance(v.__file__, str):
97 continue
98 if v.__file__.startswith(code.co_filename):
99 mname = _fn2mod[code.co_filename] = k
100 break
101 else:
102 mname = _fn2mod[code.co_filename] = '<%s>'%code.co_filename
103
104 return '%s:%d(%s)' % (mname, code.co_firstlineno, code.co_name)
105
106
107if __name__ == '__main__':
108 import os
109 sys.argv = sys.argv[1:]
110 if not sys.argv:
111 print >> sys.stderr, "usage: lsprof.py <script> <arguments...>"
112 sys.exit(2)
113 sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])))
114 stats = profile(execfile, sys.argv[0], globals(), locals())
115 stats.sort()
116 stats.pprint()
diff --git a/bitbake/lib/bb/pysh/pysh.py b/bitbake/lib/bb/pysh/pysh.py
new file mode 100644
index 0000000000..b4e6145b51
--- /dev/null
+++ b/bitbake/lib/bb/pysh/pysh.py
@@ -0,0 +1,167 @@
1# pysh.py - command processing for pysh.
2#
3# Copyright 2007 Patrick Mezard
4#
5# This software may be used and distributed according to the terms
6# of the GNU General Public License, incorporated herein by reference.
7
8import optparse
9import os
10import sys
11
12import interp
13
14SH_OPT = optparse.OptionParser(prog='pysh', usage="%prog [OPTIONS]", version='0.1')
15SH_OPT.add_option('-c', action='store_true', dest='command_string', default=None,
16 help='A string that shall be interpreted by the shell as one or more commands')
17SH_OPT.add_option('--redirect-to', dest='redirect_to', default=None,
18 help='Redirect script commands stdout and stderr to the specified file')
19# See utility_command in builtin.py about the reason for this flag.
20SH_OPT.add_option('--redirected', dest='redirected', action='store_true', default=False,
21 help='Tell the interpreter that stdout and stderr are actually the same objects, which is really stdout')
22SH_OPT.add_option('--debug-parsing', action='store_true', dest='debug_parsing', default=False,
23 help='Trace PLY execution')
24SH_OPT.add_option('--debug-tree', action='store_true', dest='debug_tree', default=False,
25 help='Display the generated syntax tree.')
26SH_OPT.add_option('--debug-cmd', action='store_true', dest='debug_cmd', default=False,
27 help='Trace command execution before parameters expansion and exit status.')
28SH_OPT.add_option('--debug-utility', action='store_true', dest='debug_utility', default=False,
29 help='Trace utility calls, after parameters expansions')
30SH_OPT.add_option('--ast', action='store_true', dest='ast', default=False,
31 help='Encoded commands to execute in a subprocess')
32SH_OPT.add_option('--profile', action='store_true', default=False,
33 help='Profile pysh run')
34
35
36def split_args(args):
37 # Separate shell arguments from command ones
38 # Just stop at the first argument not starting with a dash. I know, this is completely broken,
39 # it ignores files starting with a dash or may take option values for command file. This is not
40 # supposed to happen for now
41 command_index = len(args)
42 for i,arg in enumerate(args):
43 if not arg.startswith('-'):
44 command_index = i
45 break
46
47 return args[:command_index], args[command_index:]
48
49
50def fixenv(env):
51 path = env.get('PATH')
52 if path is not None:
53 parts = path.split(os.pathsep)
54 # Remove Windows utilities from PATH, they are useless at best and
55 # some of them (find) may be confused with other utilities.
56 parts = [p for p in parts if 'system32' not in p.lower()]
57 env['PATH'] = os.pathsep.join(parts)
58 if env.get('HOME') is None:
59 # Several utilities, including cvsps, cannot work without
60 # a defined HOME directory.
61 env['HOME'] = os.path.expanduser('~')
62 return env
63
64def _sh(cwd, shargs, cmdargs, options, debugflags=None, env=None):
65 if os.environ.get('PYSH_TEXT') != '1':
66 import msvcrt
67 for fp in (sys.stdin, sys.stdout, sys.stderr):
68 msvcrt.setmode(fp.fileno(), os.O_BINARY)
69
70 hgbin = os.environ.get('PYSH_HGTEXT') != '1'
71
72 if debugflags is None:
73 debugflags = []
74 if options.debug_parsing: debugflags.append('debug-parsing')
75 if options.debug_utility: debugflags.append('debug-utility')
76 if options.debug_cmd: debugflags.append('debug-cmd')
77 if options.debug_tree: debugflags.append('debug-tree')
78
79 if env is None:
80 env = fixenv(dict(os.environ))
81 if cwd is None:
82 cwd = os.getcwd()
83
84 if not cmdargs:
85 # Nothing to do
86 return 0
87
88 ast = None
89 command_file = None
90 if options.command_string:
91 input = cmdargs[0]
92 if not options.ast:
93 input += '\n'
94 else:
95 args, input = interp.decodeargs(input), None
96 env, ast = args
97 cwd = env.get('PWD', cwd)
98 else:
99 command_file = cmdargs[0]
100 arguments = cmdargs[1:]
101
102 prefix = interp.resolve_shebang(command_file, ignoreshell=True)
103 if prefix:
104 input = ' '.join(prefix + [command_file] + arguments)
105 else:
106 # Read commands from file
107 f = file(command_file)
108 try:
109 # Trailing newline to help the parser
110 input = f.read() + '\n'
111 finally:
112 f.close()
113
114 redirect = None
115 try:
116 if options.redirected:
117 stdout = sys.stdout
118 stderr = stdout
119 elif options.redirect_to:
120 redirect = open(options.redirect_to, 'wb')
121 stdout = redirect
122 stderr = redirect
123 else:
124 stdout = sys.stdout
125 stderr = sys.stderr
126
127 # TODO: set arguments to environment variables
128 opts = interp.Options()
129 opts.hgbinary = hgbin
130 ip = interp.Interpreter(cwd, debugflags, stdout=stdout, stderr=stderr,
131 opts=opts)
132 try:
133 # Export given environment in shell object
134 for k,v in env.iteritems():
135 ip.get_env().export(k,v)
136 return ip.execute_script(input, ast, scriptpath=command_file)
137 finally:
138 ip.close()
139 finally:
140 if redirect is not None:
141 redirect.close()
142
143def sh(cwd=None, args=None, debugflags=None, env=None):
144 if args is None:
145 args = sys.argv[1:]
146 shargs, cmdargs = split_args(args)
147 options, shargs = SH_OPT.parse_args(shargs)
148
149 if options.profile:
150 import lsprof
151 p = lsprof.Profiler()
152 p.enable(subcalls=True)
153 try:
154 return _sh(cwd, shargs, cmdargs, options, debugflags, env)
155 finally:
156 p.disable()
157 stats = lsprof.Stats(p.getstats())
158 stats.sort()
159 stats.pprint(top=10, file=sys.stderr, climit=5)
160 else:
161 return _sh(cwd, shargs, cmdargs, options, debugflags, env)
162
163def main():
164 sys.exit(sh())
165
166if __name__=='__main__':
167 main()
diff --git a/bitbake/lib/bb/pysh/pyshlex.py b/bitbake/lib/bb/pysh/pyshlex.py
new file mode 100644
index 0000000000..b30123675c
--- /dev/null
+++ b/bitbake/lib/bb/pysh/pyshlex.py
@@ -0,0 +1,888 @@
1# pyshlex.py - PLY compatible lexer for pysh.
2#
3# Copyright 2007 Patrick Mezard
4#
5# This software may be used and distributed according to the terms
6# of the GNU General Public License, incorporated herein by reference.
7
8# TODO:
9# - review all "char in 'abc'" snippets: the empty string can be matched
10# - test line continuations within quoted/expansion strings
11# - eof is buggy wrt sublexers
12# - the lexer cannot really work in pull mode as it would be required to run
13# PLY in pull mode. It was designed to work incrementally and it would not be
14# that hard to enable pull mode.
15import re
16try:
17 s = set()
18 del s
19except NameError:
20 from Set import Set as set
21
22from ply import lex
23from sherrors import *
24
25class NeedMore(Exception):
26 pass
27
28def is_blank(c):
29 return c in (' ', '\t')
30
31_RE_DIGITS = re.compile(r'^\d+$')
32
33def are_digits(s):
34 return _RE_DIGITS.search(s) is not None
35
36_OPERATORS = dict([
37 ('&&', 'AND_IF'),
38 ('||', 'OR_IF'),
39 (';;', 'DSEMI'),
40 ('<<', 'DLESS'),
41 ('>>', 'DGREAT'),
42 ('<&', 'LESSAND'),
43 ('>&', 'GREATAND'),
44 ('<>', 'LESSGREAT'),
45 ('<<-', 'DLESSDASH'),
46 ('>|', 'CLOBBER'),
47 ('&', 'AMP'),
48 (';', 'COMMA'),
49 ('<', 'LESS'),
50 ('>', 'GREATER'),
51 ('(', 'LPARENS'),
52 (')', 'RPARENS'),
53])
54
55#Make a function to silence pychecker "Local variable shadows global"
56def make_partial_ops():
57 partials = {}
58 for k in _OPERATORS:
59 for i in range(1, len(k)+1):
60 partials[k[:i]] = None
61 return partials
62
63_PARTIAL_OPERATORS = make_partial_ops()
64
65def is_partial_op(s):
66 """Return True if s matches a non-empty subpart of an operator starting
67 at its first character.
68 """
69 return s in _PARTIAL_OPERATORS
70
71def is_op(s):
72 """If s matches an operator, returns the operator identifier. Return None
73 otherwise.
74 """
75 return _OPERATORS.get(s)
76
77_RESERVEDS = dict([
78 ('if', 'If'),
79 ('then', 'Then'),
80 ('else', 'Else'),
81 ('elif', 'Elif'),
82 ('fi', 'Fi'),
83 ('do', 'Do'),
84 ('done', 'Done'),
85 ('case', 'Case'),
86 ('esac', 'Esac'),
87 ('while', 'While'),
88 ('until', 'Until'),
89 ('for', 'For'),
90 ('{', 'Lbrace'),
91 ('}', 'Rbrace'),
92 ('!', 'Bang'),
93 ('in', 'In'),
94 ('|', 'PIPE'),
95])
96
97def get_reserved(s):
98 return _RESERVEDS.get(s)
99
100_RE_NAME = re.compile(r'^[0-9a-zA-Z_]+$')
101
102def is_name(s):
103 return _RE_NAME.search(s) is not None
104
105def find_chars(seq, chars):
106 for i,v in enumerate(seq):
107 if v in chars:
108 return i,v
109 return -1, None
110
111class WordLexer:
112 """WordLexer parse quoted or expansion expressions and return an expression
113 tree. The input string can be any well formed sequence beginning with quoting
114 or expansion character. Embedded expressions are handled recursively. The
115 resulting tree is made of lists and strings. Lists represent quoted or
116 expansion expressions. Each list first element is the opening separator,
117 the last one the closing separator. In-between can be any number of strings
118 or lists for sub-expressions. Non quoted/expansion expression can written as
119 strings or as lists with empty strings as starting and ending delimiters.
120 """
121
122 NAME_CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
123 NAME_CHARSET = dict(zip(NAME_CHARSET, NAME_CHARSET))
124
125 SPECIAL_CHARSET = '@*#?-$!0'
126
127 #Characters which can be escaped depends on the current delimiters
128 ESCAPABLE = {
129 '`': set(['$', '\\', '`']),
130 '"': set(['$', '\\', '`', '"']),
131 "'": set(),
132 }
133
134 def __init__(self, heredoc = False):
135 # _buffer is the unprocessed input characters buffer
136 self._buffer = []
137 # _stack is empty or contains a quoted list being processed
138 # (this is the DFS path to the quoted expression being evaluated).
139 self._stack = []
140 self._escapable = None
141 # True when parsing unquoted here documents
142 self._heredoc = heredoc
143
144 def add(self, data, eof=False):
145 """Feed the lexer with more data. If the quoted expression can be
146 delimited, return a tuple (expr, remaining) containing the expression
147 tree and the unconsumed data.
148 Otherwise, raise NeedMore.
149 """
150 self._buffer += list(data)
151 self._parse(eof)
152
153 result = self._stack[0]
154 remaining = ''.join(self._buffer)
155 self._stack = []
156 self._buffer = []
157 return result, remaining
158
159 def _is_escapable(self, c, delim=None):
160 if delim is None:
161 if self._heredoc:
162 # Backslashes works as if they were double quoted in unquoted
163 # here-documents
164 delim = '"'
165 else:
166 if len(self._stack)<=1:
167 return True
168 delim = self._stack[-2][0]
169
170 escapables = self.ESCAPABLE.get(delim, None)
171 return escapables is None or c in escapables
172
173 def _parse_squote(self, buf, result, eof):
174 if not buf:
175 raise NeedMore()
176 try:
177 pos = buf.index("'")
178 except ValueError:
179 raise NeedMore()
180 result[-1] += ''.join(buf[:pos])
181 result += ["'"]
182 return pos+1, True
183
184 def _parse_bquote(self, buf, result, eof):
185 if not buf:
186 raise NeedMore()
187
188 if buf[0]=='\n':
189 #Remove line continuations
190 result[:] = ['', '', '']
191 elif self._is_escapable(buf[0]):
192 result[-1] += buf[0]
193 result += ['']
194 else:
195 #Keep as such
196 result[:] = ['', '\\'+buf[0], '']
197
198 return 1, True
199
200 def _parse_dquote(self, buf, result, eof):
201 if not buf:
202 raise NeedMore()
203 pos, sep = find_chars(buf, '$\\`"')
204 if pos==-1:
205 raise NeedMore()
206
207 result[-1] += ''.join(buf[:pos])
208 if sep=='"':
209 result += ['"']
210 return pos+1, True
211 else:
212 #Keep everything until the separator and defer processing
213 return pos, False
214
215 def _parse_command(self, buf, result, eof):
216 if not buf:
217 raise NeedMore()
218
219 chars = '$\\`"\''
220 if result[0] == '$(':
221 chars += ')'
222 pos, sep = find_chars(buf, chars)
223 if pos == -1:
224 raise NeedMore()
225
226 result[-1] += ''.join(buf[:pos])
227 if (result[0]=='$(' and sep==')') or (result[0]=='`' and sep=='`'):
228 result += [sep]
229 return pos+1, True
230 else:
231 return pos, False
232
233 def _parse_parameter(self, buf, result, eof):
234 if not buf:
235 raise NeedMore()
236
237 pos, sep = find_chars(buf, '$\\`"\'}')
238 if pos==-1:
239 raise NeedMore()
240
241 result[-1] += ''.join(buf[:pos])
242 if sep=='}':
243 result += [sep]
244 return pos+1, True
245 else:
246 return pos, False
247
248 def _parse_dollar(self, buf, result, eof):
249 sep = result[0]
250 if sep=='$':
251 if not buf:
252 #TODO: handle empty $
253 raise NeedMore()
254 if buf[0]=='(':
255 if len(buf)==1:
256 raise NeedMore()
257
258 if buf[1]=='(':
259 result[0] = '$(('
260 buf[:2] = []
261 else:
262 result[0] = '$('
263 buf[:1] = []
264
265 elif buf[0]=='{':
266 result[0] = '${'
267 buf[:1] = []
268 else:
269 if buf[0] in self.SPECIAL_CHARSET:
270 result[-1] = buf[0]
271 read = 1
272 else:
273 for read,c in enumerate(buf):
274 if c not in self.NAME_CHARSET:
275 break
276 else:
277 if not eof:
278 raise NeedMore()
279 read += 1
280
281 result[-1] += ''.join(buf[0:read])
282
283 if not result[-1]:
284 result[:] = ['', result[0], '']
285 else:
286 result += ['']
287 return read,True
288
289 sep = result[0]
290 if sep=='$(':
291 parsefunc = self._parse_command
292 elif sep=='${':
293 parsefunc = self._parse_parameter
294 else:
295 raise NotImplementedError(sep)
296
297 pos, closed = parsefunc(buf, result, eof)
298 return pos, closed
299
300 def _parse(self, eof):
301 buf = self._buffer
302 stack = self._stack
303 recurse = False
304
305 while 1:
306 if not stack or recurse:
307 if not buf:
308 raise NeedMore()
309 if buf[0] not in ('"\\`$\''):
310 raise ShellSyntaxError('Invalid quoted string sequence')
311 stack.append([buf[0], ''])
312 buf[:1] = []
313 recurse = False
314
315 result = stack[-1]
316 if result[0]=="'":
317 parsefunc = self._parse_squote
318 elif result[0]=='\\':
319 parsefunc = self._parse_bquote
320 elif result[0]=='"':
321 parsefunc = self._parse_dquote
322 elif result[0]=='`':
323 parsefunc = self._parse_command
324 elif result[0][0]=='$':
325 parsefunc = self._parse_dollar
326 else:
327 raise NotImplementedError()
328
329 read, closed = parsefunc(buf, result, eof)
330
331 buf[:read] = []
332 if closed:
333 if len(stack)>1:
334 #Merge in parent expression
335 parsed = stack.pop()
336 stack[-1] += [parsed]
337 stack[-1] += ['']
338 else:
339 break
340 else:
341 recurse = True
342
343def normalize_wordtree(wtree):
344 """Fold back every literal sequence (delimited with empty strings) into
345 parent sequence.
346 """
347 def normalize(wtree):
348 result = []
349 for part in wtree[1:-1]:
350 if isinstance(part, list):
351 part = normalize(part)
352 if part[0]=='':
353 #Move the part content back at current level
354 result += part[1:-1]
355 continue
356 elif not part:
357 #Remove empty strings
358 continue
359 result.append(part)
360 if not result:
361 result = ['']
362 return [wtree[0]] + result + [wtree[-1]]
363
364 return normalize(wtree)
365
366
367def make_wordtree(token, here_document=False):
368 """Parse a delimited token and return a tree similar to the ones returned by
369 WordLexer. token may contain any combinations of expansion/quoted fields and
370 non-ones.
371 """
372 tree = ['']
373 remaining = token
374 delimiters = '\\$`'
375 if not here_document:
376 delimiters += '\'"'
377
378 while 1:
379 pos, sep = find_chars(remaining, delimiters)
380 if pos==-1:
381 tree += [remaining, '']
382 return normalize_wordtree(tree)
383 tree.append(remaining[:pos])
384 remaining = remaining[pos:]
385
386 try:
387 result, remaining = WordLexer(heredoc = here_document).add(remaining, True)
388 except NeedMore:
389 raise ShellSyntaxError('Invalid token "%s"')
390 tree.append(result)
391
392
393def wordtree_as_string(wtree):
394 """Rewrite an expression tree generated by make_wordtree as string."""
395 def visit(node, output):
396 for child in node:
397 if isinstance(child, list):
398 visit(child, output)
399 else:
400 output.append(child)
401
402 output = []
403 visit(wtree, output)
404 return ''.join(output)
405
406
407def unquote_wordtree(wtree):
408 """Fold the word tree while removing quotes everywhere. Other expansion
409 sequences are joined as such.
410 """
411 def unquote(wtree):
412 unquoted = []
413 if wtree[0] in ('', "'", '"', '\\'):
414 wtree = wtree[1:-1]
415
416 for part in wtree:
417 if isinstance(part, list):
418 part = unquote(part)
419 unquoted.append(part)
420 return ''.join(unquoted)
421
422 return unquote(wtree)
423
424
425class HereDocLexer:
426 """HereDocLexer delimits whatever comes from the here-document starting newline
427 not included to the closing delimiter line included.
428 """
429 def __init__(self, op, delim):
430 assert op in ('<<', '<<-')
431 if not delim:
432 raise ShellSyntaxError('invalid here document delimiter %s' % str(delim))
433
434 self._op = op
435 self._delim = delim
436 self._buffer = []
437 self._token = []
438
439 def add(self, data, eof):
440 """If the here-document was delimited, return a tuple (content, remaining).
441 Raise NeedMore() otherwise.
442 """
443 self._buffer += list(data)
444 self._parse(eof)
445 token = ''.join(self._token)
446 remaining = ''.join(self._buffer)
447 self._token, self._remaining = [], []
448 return token, remaining
449
450 def _parse(self, eof):
451 while 1:
452 #Look for first unescaped newline. Quotes may be ignored
453 escaped = False
454 for i,c in enumerate(self._buffer):
455 if escaped:
456 escaped = False
457 elif c=='\\':
458 escaped = True
459 elif c=='\n':
460 break
461 else:
462 i = -1
463
464 if i==-1 or self._buffer[i]!='\n':
465 if not eof:
466 raise NeedMore()
467 #No more data, maybe the last line is closing delimiter
468 line = ''.join(self._buffer)
469 eol = ''
470 self._buffer[:] = []
471 else:
472 line = ''.join(self._buffer[:i])
473 eol = self._buffer[i]
474 self._buffer[:i+1] = []
475
476 if self._op=='<<-':
477 line = line.lstrip('\t')
478
479 if line==self._delim:
480 break
481
482 self._token += [line, eol]
483 if i==-1:
484 break
485
486class Token:
487 #TODO: check this is still in use
488 OPERATOR = 'OPERATOR'
489 WORD = 'WORD'
490
491 def __init__(self):
492 self.value = ''
493 self.type = None
494
495 def __getitem__(self, key):
496 #Behave like a two elements tuple
497 if key==0:
498 return self.type
499 if key==1:
500 return self.value
501 raise IndexError(key)
502
503
504class HereDoc:
505 def __init__(self, op, name=None):
506 self.op = op
507 self.name = name
508 self.pendings = []
509
510TK_COMMA = 'COMMA'
511TK_AMPERSAND = 'AMP'
512TK_OP = 'OP'
513TK_TOKEN = 'TOKEN'
514TK_COMMENT = 'COMMENT'
515TK_NEWLINE = 'NEWLINE'
516TK_IONUMBER = 'IO_NUMBER'
517TK_ASSIGNMENT = 'ASSIGNMENT_WORD'
518TK_HERENAME = 'HERENAME'
519
520class Lexer:
521 """Main lexer.
522
523 Call add() until the script AST is returned.
524 """
525 # Here-document handling makes the whole thing more complex because they basically
526 # force tokens to be reordered: here-content must come right after the operator
527 # and the here-document name, while some other tokens might be following the
528 # here-document expression on the same line.
529 #
530 # So, here-doc states are basically:
531 # *self._state==ST_NORMAL
532 # - self._heredoc.op is None: no here-document
533 # - self._heredoc.op is not None but name is: here-document operator matched,
534 # waiting for the document name/delimiter
535 # - self._heredoc.op and name are not None: here-document is ready, following
536 # tokens are being stored and will be pushed again when the document is
537 # completely parsed.
538 # *self._state==ST_HEREDOC
539 # - The here-document is being delimited by self._herelexer. Once it is done
540 # the content is pushed in front of the pending token list then all these
541 # tokens are pushed once again.
542 ST_NORMAL = 'ST_NORMAL'
543 ST_OP = 'ST_OP'
544 ST_BACKSLASH = 'ST_BACKSLASH'
545 ST_QUOTED = 'ST_QUOTED'
546 ST_COMMENT = 'ST_COMMENT'
547 ST_HEREDOC = 'ST_HEREDOC'
548
549 #Match end of backquote strings
550 RE_BACKQUOTE_END = re.compile(r'(?<!\\)(`)')
551
552 def __init__(self, parent_state = None):
553 self._input = []
554 self._pos = 0
555
556 self._token = ''
557 self._type = TK_TOKEN
558
559 self._state = self.ST_NORMAL
560 self._parent_state = parent_state
561 self._wordlexer = None
562
563 self._heredoc = HereDoc(None)
564 self._herelexer = None
565
566 ### Following attributes are not used for delimiting token and can safely
567 ### be changed after here-document detection (see _push_toke)
568
569 # Count the number of tokens following a 'For' reserved word. Needed to
570 # return an 'In' reserved word if it comes in third place.
571 self._for_count = None
572
573 def add(self, data, eof=False):
574 """Feed the lexer with data.
575
576 When eof is set to True, returns unconsumed data or raise if the lexer
577 is in the middle of a delimiting operation.
578 Raise NeedMore otherwise.
579 """
580 self._input += list(data)
581 self._parse(eof)
582 self._input[:self._pos] = []
583 return ''.join(self._input)
584
585 def _parse(self, eof):
586 while self._state:
587 if self._pos>=len(self._input):
588 if not eof:
589 raise NeedMore()
590 elif self._state not in (self.ST_OP, self.ST_QUOTED, self.ST_HEREDOC):
591 #Delimit the current token and leave cleanly
592 self._push_token('')
593 break
594 else:
595 #Let the sublexer handle the eof themselves
596 pass
597
598 if self._state==self.ST_NORMAL:
599 self._parse_normal()
600 elif self._state==self.ST_COMMENT:
601 self._parse_comment()
602 elif self._state==self.ST_OP:
603 self._parse_op(eof)
604 elif self._state==self.ST_QUOTED:
605 self._parse_quoted(eof)
606 elif self._state==self.ST_HEREDOC:
607 self._parse_heredoc(eof)
608 else:
609 assert False, "Unknown state " + str(self._state)
610
611 if self._heredoc.op is not None:
612 raise ShellSyntaxError('missing here-document delimiter')
613
614 def _parse_normal(self):
615 c = self._input[self._pos]
616 if c=='\n':
617 self._push_token(c)
618 self._token = c
619 self._type = TK_NEWLINE
620 self._push_token('')
621 self._pos += 1
622 elif c in ('\\', '\'', '"', '`', '$'):
623 self._state = self.ST_QUOTED
624 elif is_partial_op(c):
625 self._push_token(c)
626
627 self._type = TK_OP
628 self._token += c
629 self._pos += 1
630 self._state = self.ST_OP
631 elif is_blank(c):
632 self._push_token(c)
633
634 #Discard blanks
635 self._pos += 1
636 elif self._token:
637 self._token += c
638 self._pos += 1
639 elif c=='#':
640 self._state = self.ST_COMMENT
641 self._type = TK_COMMENT
642 self._pos += 1
643 else:
644 self._pos += 1
645 self._token += c
646
647 def _parse_op(self, eof):
648 assert self._token
649
650 while 1:
651 if self._pos>=len(self._input):
652 if not eof:
653 raise NeedMore()
654 c = ''
655 else:
656 c = self._input[self._pos]
657
658 op = self._token + c
659 if c and is_partial_op(op):
660 #Still parsing an operator
661 self._token = op
662 self._pos += 1
663 else:
664 #End of operator
665 self._push_token(c)
666 self._state = self.ST_NORMAL
667 break
668
669 def _parse_comment(self):
670 while 1:
671 if self._pos>=len(self._input):
672 raise NeedMore()
673
674 c = self._input[self._pos]
675 if c=='\n':
676 #End of comment, do not consume the end of line
677 self._state = self.ST_NORMAL
678 break
679 else:
680 self._token += c
681 self._pos += 1
682
683 def _parse_quoted(self, eof):
684 """Precondition: the starting backquote/dollar is still in the input queue."""
685 if not self._wordlexer:
686 self._wordlexer = WordLexer()
687
688 if self._pos<len(self._input):
689 #Transfer input queue character into the subparser
690 input = self._input[self._pos:]
691 self._pos += len(input)
692
693 wtree, remaining = self._wordlexer.add(input, eof)
694 self._wordlexer = None
695 self._token += wordtree_as_string(wtree)
696
697 #Put unparsed character back in the input queue
698 if remaining:
699 self._input[self._pos:self._pos] = list(remaining)
700 self._state = self.ST_NORMAL
701
702 def _parse_heredoc(self, eof):
703 assert not self._token
704
705 if self._herelexer is None:
706 self._herelexer = HereDocLexer(self._heredoc.op, self._heredoc.name)
707
708 if self._pos<len(self._input):
709 #Transfer input queue character into the subparser
710 input = self._input[self._pos:]
711 self._pos += len(input)
712
713 self._token, remaining = self._herelexer.add(input, eof)
714
715 #Reset here-document state
716 self._herelexer = None
717 heredoc, self._heredoc = self._heredoc, HereDoc(None)
718 if remaining:
719 self._input[self._pos:self._pos] = list(remaining)
720 self._state = self.ST_NORMAL
721
722 #Push pending tokens
723 heredoc.pendings[:0] = [(self._token, self._type, heredoc.name)]
724 for token, type, delim in heredoc.pendings:
725 self._token = token
726 self._type = type
727 self._push_token(delim)
728
729 def _push_token(self, delim):
730 if not self._token:
731 return 0
732
733 if self._heredoc.op is not None:
734 if self._heredoc.name is None:
735 #Here-document name
736 if self._type!=TK_TOKEN:
737 raise ShellSyntaxError("expecting here-document name, got '%s'" % self._token)
738 self._heredoc.name = unquote_wordtree(make_wordtree(self._token))
739 self._type = TK_HERENAME
740 else:
741 #Capture all tokens until the newline starting the here-document
742 if self._type==TK_NEWLINE:
743 assert self._state==self.ST_NORMAL
744 self._state = self.ST_HEREDOC
745
746 self._heredoc.pendings.append((self._token, self._type, delim))
747 self._token = ''
748 self._type = TK_TOKEN
749 return 1
750
751 # BEWARE: do not change parser state from here to the end of the function:
752 # when parsing between an here-document operator to the end of the line
753 # tokens are stored in self._heredoc.pendings. Therefore, they will not
754 # reach the section below.
755
756 #Check operators
757 if self._type==TK_OP:
758 #False positive because of partial op matching
759 op = is_op(self._token)
760 if not op:
761 self._type = TK_TOKEN
762 else:
763 #Map to the specific operator
764 self._type = op
765 if self._token in ('<<', '<<-'):
766 #Done here rather than in _parse_op because there is no need
767 #to change the parser state since we are still waiting for
768 #the here-document name
769 if self._heredoc.op is not None:
770 raise ShellSyntaxError("syntax error near token '%s'" % self._token)
771 assert self._heredoc.op is None
772 self._heredoc.op = self._token
773
774 if self._type==TK_TOKEN:
775 if '=' in self._token and not delim:
776 if self._token.startswith('='):
777 #Token is a WORD... a TOKEN that is.
778 pass
779 else:
780 prev = self._token[:self._token.find('=')]
781 if is_name(prev):
782 self._type = TK_ASSIGNMENT
783 else:
784 #Just a token (unspecified)
785 pass
786 else:
787 reserved = get_reserved(self._token)
788 if reserved is not None:
789 if reserved=='In' and self._for_count!=2:
790 #Sorry, not a reserved word after all
791 pass
792 else:
793 self._type = reserved
794 if reserved in ('For', 'Case'):
795 self._for_count = 0
796 elif are_digits(self._token) and delim in ('<', '>'):
797 #Detect IO_NUMBER
798 self._type = TK_IONUMBER
799 elif self._token==';':
800 self._type = TK_COMMA
801 elif self._token=='&':
802 self._type = TK_AMPERSAND
803 elif self._type==TK_COMMENT:
804 #Comments are not part of sh grammar, ignore them
805 self._token = ''
806 self._type = TK_TOKEN
807 return 0
808
809 if self._for_count is not None:
810 #Track token count in 'For' expression to detect 'In' reserved words.
811 #Can only be in third position, no need to go beyond
812 self._for_count += 1
813 if self._for_count==3:
814 self._for_count = None
815
816 self.on_token((self._token, self._type))
817 self._token = ''
818 self._type = TK_TOKEN
819 return 1
820
821 def on_token(self, token):
822 raise NotImplementedError
823
824
825tokens = [
826 TK_TOKEN,
827# To silence yacc unused token warnings
828# TK_COMMENT,
829 TK_NEWLINE,
830 TK_IONUMBER,
831 TK_ASSIGNMENT,
832 TK_HERENAME,
833]
834
835#Add specific operators
836tokens += _OPERATORS.values()
837#Add reserved words
838tokens += _RESERVEDS.values()
839
840class PLYLexer(Lexer):
841 """Bridge Lexer and PLY lexer interface."""
842 def __init__(self):
843 Lexer.__init__(self)
844 self._tokens = []
845 self._current = 0
846 self.lineno = 0
847
848 def on_token(self, token):
849 value, type = token
850
851 self.lineno = 0
852 t = lex.LexToken()
853 t.value = value
854 t.type = type
855 t.lexer = self
856 t.lexpos = 0
857 t.lineno = 0
858
859 self._tokens.append(t)
860
861 def is_empty(self):
862 return not bool(self._tokens)
863
864 #PLY compliant interface
865 def token(self):
866 if self._current>=len(self._tokens):
867 return None
868 t = self._tokens[self._current]
869 self._current += 1
870 return t
871
872
873def get_tokens(s):
874 """Parse the input string and return a tuple (tokens, unprocessed) where
875 tokens is a list of parsed tokens and unprocessed is the part of the input
876 string left untouched by the lexer.
877 """
878 lexer = PLYLexer()
879 untouched = lexer.add(s, True)
880 tokens = []
881 while 1:
882 token = lexer.token()
883 if token is None:
884 break
885 tokens.append(token)
886
887 tokens = [(t.value, t.type) for t in tokens]
888 return tokens, untouched
diff --git a/bitbake/lib/bb/pysh/pyshyacc.py b/bitbake/lib/bb/pysh/pyshyacc.py
new file mode 100644
index 0000000000..e8e80aac45
--- /dev/null
+++ b/bitbake/lib/bb/pysh/pyshyacc.py
@@ -0,0 +1,779 @@
1# pyshyacc.py - PLY grammar definition for pysh
2#
3# Copyright 2007 Patrick Mezard
4#
5# This software may be used and distributed according to the terms
6# of the GNU General Public License, incorporated herein by reference.
7
8"""PLY grammar file.
9"""
10import os.path
11import sys
12
13import pyshlex
14tokens = pyshlex.tokens
15
16from ply import yacc
17import sherrors
18
19class IORedirect:
20 def __init__(self, op, filename, io_number=None):
21 self.op = op
22 self.filename = filename
23 self.io_number = io_number
24
25class HereDocument:
26 def __init__(self, op, name, content, io_number=None):
27 self.op = op
28 self.name = name
29 self.content = content
30 self.io_number = io_number
31
32def make_io_redirect(p):
33 """Make an IORedirect instance from the input 'io_redirect' production."""
34 name, io_number, io_target = p
35 assert name=='io_redirect'
36
37 if io_target[0]=='io_file':
38 io_type, io_op, io_file = io_target
39 return IORedirect(io_op, io_file, io_number)
40 elif io_target[0]=='io_here':
41 io_type, io_op, io_name, io_content = io_target
42 return HereDocument(io_op, io_name, io_content, io_number)
43 else:
44 assert False, "Invalid IO redirection token %s" % repr(io_type)
45
46class SimpleCommand:
47 """
48 assigns contains (name, value) pairs.
49 """
50 def __init__(self, words, redirs, assigns):
51 self.words = list(words)
52 self.redirs = list(redirs)
53 self.assigns = list(assigns)
54
55class Pipeline:
56 def __init__(self, commands, reverse_status=False):
57 self.commands = list(commands)
58 assert self.commands #Grammar forbids this
59 self.reverse_status = reverse_status
60
61class AndOr:
62 def __init__(self, op, left, right):
63 self.op = str(op)
64 self.left = left
65 self.right = right
66
67class ForLoop:
68 def __init__(self, name, items, cmds):
69 self.name = str(name)
70 self.items = list(items)
71 self.cmds = list(cmds)
72
73class WhileLoop:
74 def __init__(self, condition, cmds):
75 self.condition = list(condition)
76 self.cmds = list(cmds)
77
78class UntilLoop:
79 def __init__(self, condition, cmds):
80 self.condition = list(condition)
81 self.cmds = list(cmds)
82
83class FunDef:
84 def __init__(self, name, body):
85 self.name = str(name)
86 self.body = body
87
88class BraceGroup:
89 def __init__(self, cmds):
90 self.cmds = list(cmds)
91
92class IfCond:
93 def __init__(self, cond, if_cmds, else_cmds):
94 self.cond = list(cond)
95 self.if_cmds = if_cmds
96 self.else_cmds = else_cmds
97
98class Case:
99 def __init__(self, name, items):
100 self.name = name
101 self.items = items
102
103class SubShell:
104 def __init__(self, cmds):
105 self.cmds = cmds
106
107class RedirectList:
108 def __init__(self, cmd, redirs):
109 self.cmd = cmd
110 self.redirs = list(redirs)
111
112def get_production(productions, ptype):
113 """productions must be a list of production tuples like (name, obj) where
114 name is the production string identifier.
115 Return the first production named 'ptype'. Raise KeyError if None can be
116 found.
117 """
118 for production in productions:
119 if production is not None and production[0]==ptype:
120 return production
121 raise KeyError(ptype)
122
123#-------------------------------------------------------------------------------
124# PLY grammar definition
125#-------------------------------------------------------------------------------
126
127def p_multiple_commands(p):
128 """multiple_commands : newline_sequence
129 | complete_command
130 | multiple_commands complete_command"""
131 if len(p)==2:
132 if p[1] is not None:
133 p[0] = [p[1]]
134 else:
135 p[0] = []
136 else:
137 p[0] = p[1] + [p[2]]
138
139def p_complete_command(p):
140 """complete_command : list separator
141 | list"""
142 if len(p)==3 and p[2] and p[2][1] == '&':
143 p[0] = ('async', p[1])
144 else:
145 p[0] = p[1]
146
147def p_list(p):
148 """list : list separator_op and_or
149 | and_or"""
150 if len(p)==2:
151 p[0] = [p[1]]
152 else:
153 #if p[2]!=';':
154 # raise NotImplementedError('AND-OR list asynchronous execution is not implemented')
155 p[0] = p[1] + [p[3]]
156
157def p_and_or(p):
158 """and_or : pipeline
159 | and_or AND_IF linebreak pipeline
160 | and_or OR_IF linebreak pipeline"""
161 if len(p)==2:
162 p[0] = p[1]
163 else:
164 p[0] = ('and_or', AndOr(p[2], p[1], p[4]))
165
166def p_maybe_bang_word(p):
167 """maybe_bang_word : Bang"""
168 p[0] = ('maybe_bang_word', p[1])
169
170def p_pipeline(p):
171 """pipeline : pipe_sequence
172 | bang_word pipe_sequence"""
173 if len(p)==3:
174 p[0] = ('pipeline', Pipeline(p[2][1:], True))
175 else:
176 p[0] = ('pipeline', Pipeline(p[1][1:]))
177
178def p_pipe_sequence(p):
179 """pipe_sequence : command
180 | pipe_sequence PIPE linebreak command"""
181 if len(p)==2:
182 p[0] = ['pipe_sequence', p[1]]
183 else:
184 p[0] = p[1] + [p[4]]
185
186def p_command(p):
187 """command : simple_command
188 | compound_command
189 | compound_command redirect_list
190 | function_definition"""
191
192 if p[1][0] in ( 'simple_command',
193 'for_clause',
194 'while_clause',
195 'until_clause',
196 'case_clause',
197 'if_clause',
198 'function_definition',
199 'subshell',
200 'brace_group',):
201 if len(p) == 2:
202 p[0] = p[1]
203 else:
204 p[0] = ('redirect_list', RedirectList(p[1], p[2][1:]))
205 else:
206 raise NotImplementedError('%s command is not implemented' % repr(p[1][0]))
207
208def p_compound_command(p):
209 """compound_command : brace_group
210 | subshell
211 | for_clause
212 | case_clause
213 | if_clause
214 | while_clause
215 | until_clause"""
216 p[0] = p[1]
217
218def p_subshell(p):
219 """subshell : LPARENS compound_list RPARENS"""
220 p[0] = ('subshell', SubShell(p[2][1:]))
221
222def p_compound_list(p):
223 """compound_list : term
224 | newline_list term
225 | term separator
226 | newline_list term separator"""
227 productions = p[1:]
228 try:
229 sep = get_production(productions, 'separator')
230 if sep[1]!=';':
231 raise NotImplementedError()
232 except KeyError:
233 pass
234 term = get_production(productions, 'term')
235 p[0] = ['compound_list'] + term[1:]
236
237def p_term(p):
238 """term : term separator and_or
239 | and_or"""
240 if len(p)==2:
241 p[0] = ['term', p[1]]
242 else:
243 if p[2] is not None and p[2][1] == '&':
244 p[0] = ['term', ('async', p[1][1:])] + [p[3]]
245 else:
246 p[0] = p[1] + [p[3]]
247
248def p_maybe_for_word(p):
249 # Rearrange 'For' priority wrt TOKEN. See p_for_word
250 """maybe_for_word : For"""
251 p[0] = ('maybe_for_word', p[1])
252
253def p_for_clause(p):
254 """for_clause : for_word name linebreak do_group
255 | for_word name linebreak in sequential_sep do_group
256 | for_word name linebreak in wordlist sequential_sep do_group"""
257 productions = p[1:]
258 do_group = get_production(productions, 'do_group')
259 try:
260 items = get_production(productions, 'in')[1:]
261 except KeyError:
262 raise NotImplementedError('"in" omission is not implemented')
263
264 try:
265 items = get_production(productions, 'wordlist')[1:]
266 except KeyError:
267 items = []
268
269 name = p[2]
270 p[0] = ('for_clause', ForLoop(name, items, do_group[1:]))
271
272def p_name(p):
273 """name : token""" #Was NAME instead of token
274 p[0] = p[1]
275
276def p_in(p):
277 """in : In"""
278 p[0] = ('in', p[1])
279
280def p_wordlist(p):
281 """wordlist : wordlist token
282 | token"""
283 if len(p)==2:
284 p[0] = ['wordlist', ('TOKEN', p[1])]
285 else:
286 p[0] = p[1] + [('TOKEN', p[2])]
287
288def p_case_clause(p):
289 """case_clause : Case token linebreak in linebreak case_list Esac
290 | Case token linebreak in linebreak case_list_ns Esac
291 | Case token linebreak in linebreak Esac"""
292 if len(p) < 8:
293 items = []
294 else:
295 items = p[6][1:]
296 name = p[2]
297 p[0] = ('case_clause', Case(name, [c[1] for c in items]))
298
299def p_case_list_ns(p):
300 """case_list_ns : case_list case_item_ns
301 | case_item_ns"""
302 p_case_list(p)
303
304def p_case_list(p):
305 """case_list : case_list case_item
306 | case_item"""
307 if len(p)==2:
308 p[0] = ['case_list', p[1]]
309 else:
310 p[0] = p[1] + [p[2]]
311
312def p_case_item_ns(p):
313 """case_item_ns : pattern RPARENS linebreak
314 | pattern RPARENS compound_list linebreak
315 | LPARENS pattern RPARENS linebreak
316 | LPARENS pattern RPARENS compound_list linebreak"""
317 p_case_item(p)
318
319def p_case_item(p):
320 """case_item : pattern RPARENS linebreak DSEMI linebreak
321 | pattern RPARENS compound_list DSEMI linebreak
322 | LPARENS pattern RPARENS linebreak DSEMI linebreak
323 | LPARENS pattern RPARENS compound_list DSEMI linebreak"""
324 if len(p) < 7:
325 name = p[1][1:]
326 else:
327 name = p[2][1:]
328
329 try:
330 cmds = get_production(p[1:], "compound_list")[1:]
331 except KeyError:
332 cmds = []
333
334 p[0] = ('case_item', (name, cmds))
335
336def p_pattern(p):
337 """pattern : token
338 | pattern PIPE token"""
339 if len(p)==2:
340 p[0] = ['pattern', ('TOKEN', p[1])]
341 else:
342 p[0] = p[1] + [('TOKEN', p[2])]
343
344def p_maybe_if_word(p):
345 # Rearrange 'If' priority wrt TOKEN. See p_if_word
346 """maybe_if_word : If"""
347 p[0] = ('maybe_if_word', p[1])
348
349def p_maybe_then_word(p):
350 # Rearrange 'Then' priority wrt TOKEN. See p_then_word
351 """maybe_then_word : Then"""
352 p[0] = ('maybe_then_word', p[1])
353
354def p_if_clause(p):
355 """if_clause : if_word compound_list then_word compound_list else_part Fi
356 | if_word compound_list then_word compound_list Fi"""
357 else_part = []
358 if len(p)==7:
359 else_part = p[5]
360 p[0] = ('if_clause', IfCond(p[2][1:], p[4][1:], else_part))
361
362def p_else_part(p):
363 """else_part : Elif compound_list then_word compound_list else_part
364 | Elif compound_list then_word compound_list
365 | Else compound_list"""
366 if len(p)==3:
367 p[0] = p[2][1:]
368 else:
369 else_part = []
370 if len(p)==6:
371 else_part = p[5]
372 p[0] = ('elif', IfCond(p[2][1:], p[4][1:], else_part))
373
374def p_while_clause(p):
375 """while_clause : While compound_list do_group"""
376 p[0] = ('while_clause', WhileLoop(p[2][1:], p[3][1:]))
377
378def p_maybe_until_word(p):
379 # Rearrange 'Until' priority wrt TOKEN. See p_until_word
380 """maybe_until_word : Until"""
381 p[0] = ('maybe_until_word', p[1])
382
383def p_until_clause(p):
384 """until_clause : until_word compound_list do_group"""
385 p[0] = ('until_clause', UntilLoop(p[2][1:], p[3][1:]))
386
387def p_function_definition(p):
388 """function_definition : fname LPARENS RPARENS linebreak function_body"""
389 p[0] = ('function_definition', FunDef(p[1], p[5]))
390
391def p_function_body(p):
392 """function_body : compound_command
393 | compound_command redirect_list"""
394 if len(p)!=2:
395 raise NotImplementedError('functions redirections lists are not implemented')
396 p[0] = p[1]
397
398def p_fname(p):
399 """fname : TOKEN""" #Was NAME instead of token
400 p[0] = p[1]
401
402def p_brace_group(p):
403 """brace_group : Lbrace compound_list Rbrace"""
404 p[0] = ('brace_group', BraceGroup(p[2][1:]))
405
406def p_maybe_done_word(p):
407 #See p_assignment_word for details.
408 """maybe_done_word : Done"""
409 p[0] = ('maybe_done_word', p[1])
410
411def p_maybe_do_word(p):
412 """maybe_do_word : Do"""
413 p[0] = ('maybe_do_word', p[1])
414
415def p_do_group(p):
416 """do_group : do_word compound_list done_word"""
417 #Do group contains a list of AndOr
418 p[0] = ['do_group'] + p[2][1:]
419
420def p_simple_command(p):
421 """simple_command : cmd_prefix cmd_word cmd_suffix
422 | cmd_prefix cmd_word
423 | cmd_prefix
424 | cmd_name cmd_suffix
425 | cmd_name"""
426 words, redirs, assigns = [], [], []
427 for e in p[1:]:
428 name = e[0]
429 if name in ('cmd_prefix', 'cmd_suffix'):
430 for sube in e[1:]:
431 subname = sube[0]
432 if subname=='io_redirect':
433 redirs.append(make_io_redirect(sube))
434 elif subname=='ASSIGNMENT_WORD':
435 assigns.append(sube)
436 else:
437 words.append(sube)
438 elif name in ('cmd_word', 'cmd_name'):
439 words.append(e)
440
441 cmd = SimpleCommand(words, redirs, assigns)
442 p[0] = ('simple_command', cmd)
443
444def p_cmd_name(p):
445 """cmd_name : TOKEN"""
446 p[0] = ('cmd_name', p[1])
447
448def p_cmd_word(p):
449 """cmd_word : token"""
450 p[0] = ('cmd_word', p[1])
451
452def p_maybe_assignment_word(p):
453 #See p_assignment_word for details.
454 """maybe_assignment_word : ASSIGNMENT_WORD"""
455 p[0] = ('maybe_assignment_word', p[1])
456
457def p_cmd_prefix(p):
458 """cmd_prefix : io_redirect
459 | cmd_prefix io_redirect
460 | assignment_word
461 | cmd_prefix assignment_word"""
462 try:
463 prefix = get_production(p[1:], 'cmd_prefix')
464 except KeyError:
465 prefix = ['cmd_prefix']
466
467 try:
468 value = get_production(p[1:], 'assignment_word')[1]
469 value = ('ASSIGNMENT_WORD', value.split('=', 1))
470 except KeyError:
471 value = get_production(p[1:], 'io_redirect')
472 p[0] = prefix + [value]
473
474def p_cmd_suffix(p):
475 """cmd_suffix : io_redirect
476 | cmd_suffix io_redirect
477 | token
478 | cmd_suffix token
479 | maybe_for_word
480 | cmd_suffix maybe_for_word
481 | maybe_done_word
482 | cmd_suffix maybe_done_word
483 | maybe_do_word
484 | cmd_suffix maybe_do_word
485 | maybe_until_word
486 | cmd_suffix maybe_until_word
487 | maybe_assignment_word
488 | cmd_suffix maybe_assignment_word
489 | maybe_if_word
490 | cmd_suffix maybe_if_word
491 | maybe_then_word
492 | cmd_suffix maybe_then_word
493 | maybe_bang_word
494 | cmd_suffix maybe_bang_word"""
495 try:
496 suffix = get_production(p[1:], 'cmd_suffix')
497 token = p[2]
498 except KeyError:
499 suffix = ['cmd_suffix']
500 token = p[1]
501
502 if isinstance(token, tuple):
503 if token[0]=='io_redirect':
504 p[0] = suffix + [token]
505 else:
506 #Convert maybe_* to TOKEN if necessary
507 p[0] = suffix + [('TOKEN', token[1])]
508 else:
509 p[0] = suffix + [('TOKEN', token)]
510
511def p_redirect_list(p):
512 """redirect_list : io_redirect
513 | redirect_list io_redirect"""
514 if len(p) == 2:
515 p[0] = ['redirect_list', make_io_redirect(p[1])]
516 else:
517 p[0] = p[1] + [make_io_redirect(p[2])]
518
519def p_io_redirect(p):
520 """io_redirect : io_file
521 | IO_NUMBER io_file
522 | io_here
523 | IO_NUMBER io_here"""
524 if len(p)==3:
525 p[0] = ('io_redirect', p[1], p[2])
526 else:
527 p[0] = ('io_redirect', None, p[1])
528
529def p_io_file(p):
530 #Return the tuple (operator, filename)
531 """io_file : LESS filename
532 | LESSAND filename
533 | GREATER filename
534 | GREATAND filename
535 | DGREAT filename
536 | LESSGREAT filename
537 | CLOBBER filename"""
538 #Extract the filename from the file
539 p[0] = ('io_file', p[1], p[2][1])
540
541def p_filename(p):
542 #Return the filename
543 """filename : TOKEN"""
544 p[0] = ('filename', p[1])
545
546def p_io_here(p):
547 """io_here : DLESS here_end
548 | DLESSDASH here_end"""
549 p[0] = ('io_here', p[1], p[2][1], p[2][2])
550
551def p_here_end(p):
552 """here_end : HERENAME TOKEN"""
553 p[0] = ('here_document', p[1], p[2])
554
555def p_newline_sequence(p):
556 # Nothing in the grammar can handle leading NEWLINE productions, so add
557 # this one with the lowest possible priority relatively to newline_list.
558 """newline_sequence : newline_list"""
559 p[0] = None
560
561def p_newline_list(p):
562 """newline_list : NEWLINE
563 | newline_list NEWLINE"""
564 p[0] = None
565
566def p_linebreak(p):
567 """linebreak : newline_list
568 | empty"""
569 p[0] = None
570
571def p_separator_op(p):
572 """separator_op : COMMA
573 | AMP"""
574 p[0] = p[1]
575
576def p_separator(p):
577 """separator : separator_op linebreak
578 | newline_list"""
579 if len(p)==2:
580 #Ignore newlines
581 p[0] = None
582 else:
583 #Keep the separator operator
584 p[0] = ('separator', p[1])
585
586def p_sequential_sep(p):
587 """sequential_sep : COMMA linebreak
588 | newline_list"""
589 p[0] = None
590
591# Low priority TOKEN => for_word conversion.
592# Let maybe_for_word be used as a token when necessary in higher priority
593# rules.
594def p_for_word(p):
595 """for_word : maybe_for_word"""
596 p[0] = p[1]
597
598def p_if_word(p):
599 """if_word : maybe_if_word"""
600 p[0] = p[1]
601
602def p_then_word(p):
603 """then_word : maybe_then_word"""
604 p[0] = p[1]
605
606def p_done_word(p):
607 """done_word : maybe_done_word"""
608 p[0] = p[1]
609
610def p_do_word(p):
611 """do_word : maybe_do_word"""
612 p[0] = p[1]
613
614def p_until_word(p):
615 """until_word : maybe_until_word"""
616 p[0] = p[1]
617
618def p_assignment_word(p):
619 """assignment_word : maybe_assignment_word"""
620 p[0] = ('assignment_word', p[1][1])
621
622def p_bang_word(p):
623 """bang_word : maybe_bang_word"""
624 p[0] = ('bang_word', p[1][1])
625
626def p_token(p):
627 """token : TOKEN
628 | Fi"""
629 p[0] = p[1]
630
631def p_empty(p):
632 'empty :'
633 p[0] = None
634
635# Error rule for syntax errors
636def p_error(p):
637 msg = []
638 w = msg.append
639 w('%r\n' % p)
640 w('followed by:\n')
641 for i in range(5):
642 n = yacc.token()
643 if not n:
644 break
645 w(' %r\n' % n)
646 raise sherrors.ShellSyntaxError(''.join(msg))
647
648# Build the parser
649try:
650 import pyshtables
651except ImportError:
652 outputdir = os.path.dirname(__file__)
653 if not os.access(outputdir, os.W_OK):
654 outputdir = ''
655 yacc.yacc(tabmodule = 'pyshtables', outputdir = outputdir, debug = 0)
656else:
657 yacc.yacc(tabmodule = 'pysh.pyshtables', write_tables = 0, debug = 0)
658
659
660def parse(input, eof=False, debug=False):
661 """Parse a whole script at once and return the generated AST and unconsumed
662 data in a tuple.
663
664 NOTE: eof is probably meaningless for now, the parser being unable to work
665 in pull mode. It should be set to True.
666 """
667 lexer = pyshlex.PLYLexer()
668 remaining = lexer.add(input, eof)
669 if lexer.is_empty():
670 return [], remaining
671 if debug:
672 debug = 2
673 return yacc.parse(lexer=lexer, debug=debug), remaining
674
675#-------------------------------------------------------------------------------
676# AST rendering helpers
677#-------------------------------------------------------------------------------
678
679def format_commands(v):
680 """Return a tree made of strings and lists. Make command trees easier to
681 display.
682 """
683 if isinstance(v, list):
684 return [format_commands(c) for c in v]
685 if isinstance(v, tuple):
686 if len(v)==2 and isinstance(v[0], str) and not isinstance(v[1], str):
687 if v[0] == 'async':
688 return ['AsyncList', map(format_commands, v[1])]
689 else:
690 #Avoid decomposing tuples like ('pipeline', Pipeline(...))
691 return format_commands(v[1])
692 return format_commands(list(v))
693 elif isinstance(v, IfCond):
694 name = ['IfCond']
695 name += ['if', map(format_commands, v.cond)]
696 name += ['then', map(format_commands, v.if_cmds)]
697 name += ['else', map(format_commands, v.else_cmds)]
698 return name
699 elif isinstance(v, ForLoop):
700 name = ['ForLoop']
701 name += [repr(v.name)+' in ', map(str, v.items)]
702 name += ['commands', map(format_commands, v.cmds)]
703 return name
704 elif isinstance(v, AndOr):
705 return [v.op, format_commands(v.left), format_commands(v.right)]
706 elif isinstance(v, Pipeline):
707 name = 'Pipeline'
708 if v.reverse_status:
709 name = '!' + name
710 return [name, format_commands(v.commands)]
711 elif isinstance(v, Case):
712 name = ['Case']
713 name += [v.name, format_commands(v.items)]
714 elif isinstance(v, SimpleCommand):
715 name = ['SimpleCommand']
716 if v.words:
717 name += ['words', map(str, v.words)]
718 if v.assigns:
719 assigns = [tuple(a[1]) for a in v.assigns]
720 name += ['assigns', map(str, assigns)]
721 if v.redirs:
722 name += ['redirs', map(format_commands, v.redirs)]
723 return name
724 elif isinstance(v, RedirectList):
725 name = ['RedirectList']
726 if v.redirs:
727 name += ['redirs', map(format_commands, v.redirs)]
728 name += ['command', format_commands(v.cmd)]
729 return name
730 elif isinstance(v, IORedirect):
731 return ' '.join(map(str, (v.io_number, v.op, v.filename)))
732 elif isinstance(v, HereDocument):
733 return ' '.join(map(str, (v.io_number, v.op, repr(v.name), repr(v.content))))
734 elif isinstance(v, SubShell):
735 return ['SubShell', map(format_commands, v.cmds)]
736 else:
737 return repr(v)
738
739def print_commands(cmds, output=sys.stdout):
740 """Pretty print a command tree."""
741 def print_tree(cmd, spaces, output):
742 if isinstance(cmd, list):
743 for c in cmd:
744 print_tree(c, spaces + 3, output)
745 else:
746 print >>output, ' '*spaces + str(cmd)
747
748 formatted = format_commands(cmds)
749 print_tree(formatted, 0, output)
750
751
752def stringify_commands(cmds):
753 """Serialize a command tree as a string.
754
755 Returned string is not pretty and is currently used for unit tests only.
756 """
757 def stringify(value):
758 output = []
759 if isinstance(value, list):
760 formatted = []
761 for v in value:
762 formatted.append(stringify(v))
763 formatted = ' '.join(formatted)
764 output.append(''.join(['<', formatted, '>']))
765 else:
766 output.append(value)
767 return ' '.join(output)
768
769 return stringify(format_commands(cmds))
770
771
772def visit_commands(cmds, callable):
773 """Visit the command tree and execute callable on every Pipeline and
774 SimpleCommand instances.
775 """
776 if isinstance(cmds, (tuple, list)):
777 map(lambda c: visit_commands(c,callable), cmds)
778 elif isinstance(cmds, (Pipeline, SimpleCommand)):
779 callable(cmds)
diff --git a/bitbake/lib/bb/pysh/sherrors.py b/bitbake/lib/bb/pysh/sherrors.py
new file mode 100644
index 0000000000..1d5bd53b3a
--- /dev/null
+++ b/bitbake/lib/bb/pysh/sherrors.py
@@ -0,0 +1,41 @@
1# sherrors.py - shell errors and signals
2#
3# Copyright 2007 Patrick Mezard
4#
5# This software may be used and distributed according to the terms
6# of the GNU General Public License, incorporated herein by reference.
7
8"""Define shell exceptions and error codes.
9"""
10
11class ShellError(Exception):
12 pass
13
14class ShellSyntaxError(ShellError):
15 pass
16
17class UtilityError(ShellError):
18 """Raised upon utility syntax error (option or operand error)."""
19 pass
20
21class ExpansionError(ShellError):
22 pass
23
24class CommandNotFound(ShellError):
25 """Specified command was not found."""
26 pass
27
28class RedirectionError(ShellError):
29 pass
30
31class VarAssignmentError(ShellError):
32 """Variable assignment error."""
33 pass
34
35class ExitSignal(ShellError):
36 """Exit signal."""
37 pass
38
39class ReturnSignal(ShellError):
40 """Exit signal."""
41 pass \ No newline at end of file
diff --git a/bitbake/lib/bb/pysh/subprocess_fix.py b/bitbake/lib/bb/pysh/subprocess_fix.py
new file mode 100644
index 0000000000..46eca22802
--- /dev/null
+++ b/bitbake/lib/bb/pysh/subprocess_fix.py
@@ -0,0 +1,77 @@
1# subprocess - Subprocesses with accessible I/O streams
2#
3# For more information about this module, see PEP 324.
4#
5# This module should remain compatible with Python 2.2, see PEP 291.
6#
7# Copyright (c) 2003-2005 by Peter Astrand <astrand@lysator.liu.se>
8#
9# Licensed to PSF under a Contributor Agreement.
10# See http://www.python.org/2.4/license for licensing details.
11
12def list2cmdline(seq):
13 """
14 Translate a sequence of arguments into a command line
15 string, using the same rules as the MS C runtime:
16
17 1) Arguments are delimited by white space, which is either a
18 space or a tab.
19
20 2) A string surrounded by double quotation marks is
21 interpreted as a single argument, regardless of white space
22 contained within. A quoted string can be embedded in an
23 argument.
24
25 3) A double quotation mark preceded by a backslash is
26 interpreted as a literal double quotation mark.
27
28 4) Backslashes are interpreted literally, unless they
29 immediately precede a double quotation mark.
30
31 5) If backslashes immediately precede a double quotation mark,
32 every pair of backslashes is interpreted as a literal
33 backslash. If the number of backslashes is odd, the last
34 backslash escapes the next double quotation mark as
35 described in rule 3.
36 """
37
38 # See
39 # http://msdn.microsoft.com/library/en-us/vccelng/htm/progs_12.asp
40 result = []
41 needquote = False
42 for arg in seq:
43 bs_buf = []
44
45 # Add a space to separate this argument from the others
46 if result:
47 result.append(' ')
48
49 needquote = (" " in arg) or ("\t" in arg) or ("|" in arg) or arg == ""
50 if needquote:
51 result.append('"')
52
53 for c in arg:
54 if c == '\\':
55 # Don't know if we need to double yet.
56 bs_buf.append(c)
57 elif c == '"':
58 # Double backspaces.
59 result.append('\\' * len(bs_buf)*2)
60 bs_buf = []
61 result.append('\\"')
62 else:
63 # Normal char
64 if bs_buf:
65 result.extend(bs_buf)
66 bs_buf = []
67 result.append(c)
68
69 # Add remaining backspaces, if any.
70 if bs_buf:
71 result.extend(bs_buf)
72
73 if needquote:
74 result.extend(bs_buf)
75 result.append('"')
76
77 return ''.join(result)
diff --git a/bitbake/lib/bb/runqueue.py b/bitbake/lib/bb/runqueue.py
new file mode 100644
index 0000000000..c09cfd4b2c
--- /dev/null
+++ b/bitbake/lib/bb/runqueue.py
@@ -0,0 +1,1914 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4"""
5BitBake 'RunQueue' implementation
6
7Handles preparation and execution of a queue of tasks
8"""
9
10# Copyright (C) 2006-2007 Richard Purdie
11#
12# This program is free software; you can redistribute it and/or modify
13# it under the terms of the GNU General Public License version 2 as
14# published by the Free Software Foundation.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License along
22# with this program; if not, write to the Free Software Foundation, Inc.,
23# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24
25import copy
26import os
27import sys
28import signal
29import stat
30import fcntl
31import errno
32import logging
33import bb
34from bb import msg, data, event
35from bb import monitordisk
36import subprocess
37
38try:
39 import cPickle as pickle
40except ImportError:
41 import pickle
42
43bblogger = logging.getLogger("BitBake")
44logger = logging.getLogger("BitBake.RunQueue")
45
46class RunQueueStats:
47 """
48 Holds statistics on the tasks handled by the associated runQueue
49 """
50 def __init__(self, total):
51 self.completed = 0
52 self.skipped = 0
53 self.failed = 0
54 self.active = 0
55 self.total = total
56
57 def copy(self):
58 obj = self.__class__(self.total)
59 obj.__dict__.update(self.__dict__)
60 return obj
61
62 def taskFailed(self):
63 self.active = self.active - 1
64 self.failed = self.failed + 1
65
66 def taskCompleted(self, number = 1):
67 self.active = self.active - number
68 self.completed = self.completed + number
69
70 def taskSkipped(self, number = 1):
71 self.active = self.active + number
72 self.skipped = self.skipped + number
73
74 def taskActive(self):
75 self.active = self.active + 1
76
77# These values indicate the next step due to be run in the
78# runQueue state machine
79runQueuePrepare = 2
80runQueueSceneInit = 3
81runQueueSceneRun = 4
82runQueueRunInit = 5
83runQueueRunning = 6
84runQueueFailed = 7
85runQueueCleanUp = 8
86runQueueComplete = 9
87
88class RunQueueScheduler(object):
89 """
90 Control the order tasks are scheduled in.
91 """
92 name = "basic"
93
94 def __init__(self, runqueue, rqdata):
95 """
96 The default scheduler just returns the first buildable task (the
97 priority map is sorted by task numer)
98 """
99 self.rq = runqueue
100 self.rqdata = rqdata
101 numTasks = len(self.rqdata.runq_fnid)
102
103 self.prio_map = []
104 self.prio_map.extend(range(numTasks))
105
106 def next_buildable_task(self):
107 """
108 Return the id of the first task we find that is buildable
109 """
110 for tasknum in xrange(len(self.rqdata.runq_fnid)):
111 taskid = self.prio_map[tasknum]
112 if self.rq.runq_running[taskid] == 1:
113 continue
114 if self.rq.runq_buildable[taskid] == 1:
115 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[taskid]]
116 taskname = self.rqdata.runq_task[taskid]
117 stamp = bb.build.stampfile(taskname, self.rqdata.dataCache, fn)
118 if stamp in self.rq.build_stamps.values():
119 continue
120 return taskid
121
122 def next(self):
123 """
124 Return the id of the task we should build next
125 """
126 if self.rq.stats.active < self.rq.number_tasks:
127 return self.next_buildable_task()
128
129class RunQueueSchedulerSpeed(RunQueueScheduler):
130 """
131 A scheduler optimised for speed. The priority map is sorted by task weight,
132 heavier weighted tasks (tasks needed by the most other tasks) are run first.
133 """
134 name = "speed"
135
136 def __init__(self, runqueue, rqdata):
137 """
138 The priority map is sorted by task weight.
139 """
140
141 self.rq = runqueue
142 self.rqdata = rqdata
143
144 sortweight = sorted(copy.deepcopy(self.rqdata.runq_weight))
145 copyweight = copy.deepcopy(self.rqdata.runq_weight)
146 self.prio_map = []
147
148 for weight in sortweight:
149 idx = copyweight.index(weight)
150 self.prio_map.append(idx)
151 copyweight[idx] = -1
152
153 self.prio_map.reverse()
154
155class RunQueueSchedulerCompletion(RunQueueSchedulerSpeed):
156 """
157 A scheduler optimised to complete .bb files are quickly as possible. The
158 priority map is sorted by task weight, but then reordered so once a given
159 .bb file starts to build, its completed as quickly as possible. This works
160 well where disk space is at a premium and classes like OE's rm_work are in
161 force.
162 """
163 name = "completion"
164
165 def __init__(self, runqueue, rqdata):
166 RunQueueSchedulerSpeed.__init__(self, runqueue, rqdata)
167
168 #FIXME - whilst this groups all fnids together it does not reorder the
169 #fnid groups optimally.
170
171 basemap = copy.deepcopy(self.prio_map)
172 self.prio_map = []
173 while (len(basemap) > 0):
174 entry = basemap.pop(0)
175 self.prio_map.append(entry)
176 fnid = self.rqdata.runq_fnid[entry]
177 todel = []
178 for entry in basemap:
179 entry_fnid = self.rqdata.runq_fnid[entry]
180 if entry_fnid == fnid:
181 todel.append(basemap.index(entry))
182 self.prio_map.append(entry)
183 todel.reverse()
184 for idx in todel:
185 del basemap[idx]
186
187class RunQueueData:
188 """
189 BitBake Run Queue implementation
190 """
191 def __init__(self, rq, cooker, cfgData, dataCache, taskData, targets):
192 self.cooker = cooker
193 self.dataCache = dataCache
194 self.taskData = taskData
195 self.targets = targets
196 self.rq = rq
197 self.warn_multi_bb = False
198
199 self.stampwhitelist = cfgData.getVar("BB_STAMP_WHITELIST", True) or ""
200 self.multi_provider_whitelist = (cfgData.getVar("MULTI_PROVIDER_WHITELIST", True) or "").split()
201
202 self.reset()
203
204 def reset(self):
205 self.runq_fnid = []
206 self.runq_task = []
207 self.runq_depends = []
208 self.runq_revdeps = []
209 self.runq_hash = []
210
211 def runq_depends_names(self, ids):
212 import re
213 ret = []
214 for id in self.runq_depends[ids]:
215 nam = os.path.basename(self.get_user_idstring(id))
216 nam = re.sub("_[^,]*,", ",", nam)
217 ret.extend([nam])
218 return ret
219
220 def get_task_name(self, task):
221 return self.runq_task[task]
222
223 def get_task_file(self, task):
224 return self.taskData.fn_index[self.runq_fnid[task]]
225
226 def get_task_hash(self, task):
227 return self.runq_hash[task]
228
229 def get_user_idstring(self, task, task_name_suffix = ""):
230 fn = self.taskData.fn_index[self.runq_fnid[task]]
231 taskname = self.runq_task[task] + task_name_suffix
232 return "%s, %s" % (fn, taskname)
233
234 def get_task_id(self, fnid, taskname):
235 for listid in xrange(len(self.runq_fnid)):
236 if self.runq_fnid[listid] == fnid and self.runq_task[listid] == taskname:
237 return listid
238 return None
239
240 def circular_depchains_handler(self, tasks):
241 """
242 Some tasks aren't buildable, likely due to circular dependency issues.
243 Identify the circular dependencies and print them in a user readable format.
244 """
245 from copy import deepcopy
246
247 valid_chains = []
248 explored_deps = {}
249 msgs = []
250
251 def chain_reorder(chain):
252 """
253 Reorder a dependency chain so the lowest task id is first
254 """
255 lowest = 0
256 new_chain = []
257 for entry in xrange(len(chain)):
258 if chain[entry] < chain[lowest]:
259 lowest = entry
260 new_chain.extend(chain[lowest:])
261 new_chain.extend(chain[:lowest])
262 return new_chain
263
264 def chain_compare_equal(chain1, chain2):
265 """
266 Compare two dependency chains and see if they're the same
267 """
268 if len(chain1) != len(chain2):
269 return False
270 for index in xrange(len(chain1)):
271 if chain1[index] != chain2[index]:
272 return False
273 return True
274
275 def chain_array_contains(chain, chain_array):
276 """
277 Return True if chain_array contains chain
278 """
279 for ch in chain_array:
280 if chain_compare_equal(ch, chain):
281 return True
282 return False
283
284 def find_chains(taskid, prev_chain):
285 prev_chain.append(taskid)
286 total_deps = []
287 total_deps.extend(self.runq_revdeps[taskid])
288 for revdep in self.runq_revdeps[taskid]:
289 if revdep in prev_chain:
290 idx = prev_chain.index(revdep)
291 # To prevent duplicates, reorder the chain to start with the lowest taskid
292 # and search through an array of those we've already printed
293 chain = prev_chain[idx:]
294 new_chain = chain_reorder(chain)
295 if not chain_array_contains(new_chain, valid_chains):
296 valid_chains.append(new_chain)
297 msgs.append("Dependency loop #%d found:\n" % len(valid_chains))
298 for dep in new_chain:
299 msgs.append(" Task %s (%s) (dependent Tasks %s)\n" % (dep, self.get_user_idstring(dep), self.runq_depends_names(dep)))
300 msgs.append("\n")
301 if len(valid_chains) > 10:
302 msgs.append("Aborted dependency loops search after 10 matches.\n")
303 return msgs
304 continue
305 scan = False
306 if revdep not in explored_deps:
307 scan = True
308 elif revdep in explored_deps[revdep]:
309 scan = True
310 else:
311 for dep in prev_chain:
312 if dep in explored_deps[revdep]:
313 scan = True
314 if scan:
315 find_chains(revdep, copy.deepcopy(prev_chain))
316 for dep in explored_deps[revdep]:
317 if dep not in total_deps:
318 total_deps.append(dep)
319
320 explored_deps[taskid] = total_deps
321
322 for task in tasks:
323 find_chains(task, [])
324
325 return msgs
326
327 def calculate_task_weights(self, endpoints):
328 """
329 Calculate a number representing the "weight" of each task. Heavier weighted tasks
330 have more dependencies and hence should be executed sooner for maximum speed.
331
332 This function also sanity checks the task list finding tasks that are not
333 possible to execute due to circular dependencies.
334 """
335
336 numTasks = len(self.runq_fnid)
337 weight = []
338 deps_left = []
339 task_done = []
340
341 for listid in xrange(numTasks):
342 task_done.append(False)
343 weight.append(0)
344 deps_left.append(len(self.runq_revdeps[listid]))
345
346 for listid in endpoints:
347 weight[listid] = 1
348 task_done[listid] = True
349
350 while True:
351 next_points = []
352 for listid in endpoints:
353 for revdep in self.runq_depends[listid]:
354 weight[revdep] = weight[revdep] + weight[listid]
355 deps_left[revdep] = deps_left[revdep] - 1
356 if deps_left[revdep] == 0:
357 next_points.append(revdep)
358 task_done[revdep] = True
359 endpoints = next_points
360 if len(next_points) == 0:
361 break
362
363 # Circular dependency sanity check
364 problem_tasks = []
365 for task in xrange(numTasks):
366 if task_done[task] is False or deps_left[task] != 0:
367 problem_tasks.append(task)
368 logger.debug(2, "Task %s (%s) is not buildable", task, self.get_user_idstring(task))
369 logger.debug(2, "(Complete marker was %s and the remaining dependency count was %s)\n", task_done[task], deps_left[task])
370
371 if problem_tasks:
372 message = "Unbuildable tasks were found.\n"
373 message = message + "These are usually caused by circular dependencies and any circular dependency chains found will be printed below. Increase the debug level to see a list of unbuildable tasks.\n\n"
374 message = message + "Identifying dependency loops (this may take a short while)...\n"
375 logger.error(message)
376
377 msgs = self.circular_depchains_handler(problem_tasks)
378
379 message = "\n"
380 for msg in msgs:
381 message = message + msg
382 bb.msg.fatal("RunQueue", message)
383
384 return weight
385
386 def prepare(self):
387 """
388 Turn a set of taskData into a RunQueue and compute data needed
389 to optimise the execution order.
390 """
391
392 runq_build = []
393 recursivetasks = {}
394 recursiveitasks = {}
395 recursivetasksselfref = set()
396
397 taskData = self.taskData
398
399 if len(taskData.tasks_name) == 0:
400 # Nothing to do
401 return 0
402
403 logger.info("Preparing runqueue")
404
405 # Step A - Work out a list of tasks to run
406 #
407 # Taskdata gives us a list of possible providers for every build and run
408 # target ordered by priority. It also gives information on each of those
409 # providers.
410 #
411 # To create the actual list of tasks to execute we fix the list of
412 # providers and then resolve the dependencies into task IDs. This
413 # process is repeated for each type of dependency (tdepends, deptask,
414 # rdeptast, recrdeptask, idepends).
415
416 def add_build_dependencies(depids, tasknames, depends):
417 for depid in depids:
418 # Won't be in build_targets if ASSUME_PROVIDED
419 if depid not in taskData.build_targets:
420 continue
421 depdata = taskData.build_targets[depid][0]
422 if depdata is None:
423 continue
424 for taskname in tasknames:
425 taskid = taskData.gettask_id_fromfnid(depdata, taskname)
426 if taskid is not None:
427 depends.add(taskid)
428
429 def add_runtime_dependencies(depids, tasknames, depends):
430 for depid in depids:
431 if depid not in taskData.run_targets:
432 continue
433 depdata = taskData.run_targets[depid][0]
434 if depdata is None:
435 continue
436 for taskname in tasknames:
437 taskid = taskData.gettask_id_fromfnid(depdata, taskname)
438 if taskid is not None:
439 depends.add(taskid)
440
441 def add_resolved_dependencies(depids, tasknames, depends):
442 for depid in depids:
443 for taskname in tasknames:
444 taskid = taskData.gettask_id_fromfnid(depid, taskname)
445 if taskid is not None:
446 depends.add(taskid)
447
448 for task in xrange(len(taskData.tasks_name)):
449 depends = set()
450 fnid = taskData.tasks_fnid[task]
451 fn = taskData.fn_index[fnid]
452 task_deps = self.dataCache.task_deps[fn]
453
454 logger.debug(2, "Processing %s:%s", fn, taskData.tasks_name[task])
455
456 if fnid not in taskData.failed_fnids:
457
458 # Resolve task internal dependencies
459 #
460 # e.g. addtask before X after Y
461 depends = set(taskData.tasks_tdepends[task])
462
463 # Resolve 'deptask' dependencies
464 #
465 # e.g. do_sometask[deptask] = "do_someothertask"
466 # (makes sure sometask runs after someothertask of all DEPENDS)
467 if 'deptask' in task_deps and taskData.tasks_name[task] in task_deps['deptask']:
468 tasknames = task_deps['deptask'][taskData.tasks_name[task]].split()
469 add_build_dependencies(taskData.depids[fnid], tasknames, depends)
470
471 # Resolve 'rdeptask' dependencies
472 #
473 # e.g. do_sometask[rdeptask] = "do_someothertask"
474 # (makes sure sometask runs after someothertask of all RDEPENDS)
475 if 'rdeptask' in task_deps and taskData.tasks_name[task] in task_deps['rdeptask']:
476 tasknames = task_deps['rdeptask'][taskData.tasks_name[task]].split()
477 add_runtime_dependencies(taskData.rdepids[fnid], tasknames, depends)
478
479 # Resolve inter-task dependencies
480 #
481 # e.g. do_sometask[depends] = "targetname:do_someothertask"
482 # (makes sure sometask runs after targetname's someothertask)
483 idepends = taskData.tasks_idepends[task]
484 for (depid, idependtask) in idepends:
485 if depid in taskData.build_targets and not depid in taskData.failed_deps:
486 # Won't be in build_targets if ASSUME_PROVIDED
487 depdata = taskData.build_targets[depid][0]
488 if depdata is not None:
489 taskid = taskData.gettask_id_fromfnid(depdata, idependtask)
490 if taskid is None:
491 bb.msg.fatal("RunQueue", "Task %s in %s depends upon non-existent task %s in %s" % (taskData.tasks_name[task], fn, idependtask, taskData.fn_index[depdata]))
492 depends.add(taskid)
493 irdepends = taskData.tasks_irdepends[task]
494 for (depid, idependtask) in irdepends:
495 if depid in taskData.run_targets:
496 # Won't be in run_targets if ASSUME_PROVIDED
497 depdata = taskData.run_targets[depid][0]
498 if depdata is not None:
499 taskid = taskData.gettask_id_fromfnid(depdata, idependtask)
500 if taskid is None:
501 bb.msg.fatal("RunQueue", "Task %s in %s rdepends upon non-existent task %s in %s" % (taskData.tasks_name[task], fn, idependtask, taskData.fn_index[depdata]))
502 depends.add(taskid)
503
504 # Resolve recursive 'recrdeptask' dependencies (Part A)
505 #
506 # e.g. do_sometask[recrdeptask] = "do_someothertask"
507 # (makes sure sometask runs after someothertask of all DEPENDS, RDEPENDS and intertask dependencies, recursively)
508 # We cover the recursive part of the dependencies below
509 if 'recrdeptask' in task_deps and taskData.tasks_name[task] in task_deps['recrdeptask']:
510 tasknames = task_deps['recrdeptask'][taskData.tasks_name[task]].split()
511 recursivetasks[task] = tasknames
512 add_build_dependencies(taskData.depids[fnid], tasknames, depends)
513 add_runtime_dependencies(taskData.rdepids[fnid], tasknames, depends)
514 if taskData.tasks_name[task] in tasknames:
515 recursivetasksselfref.add(task)
516
517 if 'recideptask' in task_deps and taskData.tasks_name[task] in task_deps['recideptask']:
518 recursiveitasks[task] = []
519 for t in task_deps['recideptask'][taskData.tasks_name[task]].split():
520 newdep = taskData.gettask_id_fromfnid(fnid, t)
521 recursiveitasks[task].append(newdep)
522
523 self.runq_fnid.append(taskData.tasks_fnid[task])
524 self.runq_task.append(taskData.tasks_name[task])
525 self.runq_depends.append(depends)
526 self.runq_revdeps.append(set())
527 self.runq_hash.append("")
528
529 runq_build.append(0)
530
531 # Resolve recursive 'recrdeptask' dependencies (Part B)
532 #
533 # e.g. do_sometask[recrdeptask] = "do_someothertask"
534 # (makes sure sometask runs after someothertask of all DEPENDS, RDEPENDS and intertask dependencies, recursively)
535 # We need to do this separately since we need all of self.runq_depends to be complete before this is processed
536 extradeps = {}
537 for task in recursivetasks:
538 extradeps[task] = set(self.runq_depends[task])
539 tasknames = recursivetasks[task]
540 seendeps = set()
541 seenfnid = []
542
543 def generate_recdeps(t):
544 newdeps = set()
545 add_resolved_dependencies([taskData.tasks_fnid[t]], tasknames, newdeps)
546 extradeps[task].update(newdeps)
547 seendeps.add(t)
548 newdeps.add(t)
549 for i in newdeps:
550 for n in self.runq_depends[i]:
551 if n not in seendeps:
552 generate_recdeps(n)
553 generate_recdeps(task)
554
555 if task in recursiveitasks:
556 for dep in recursiveitasks[task]:
557 generate_recdeps(dep)
558
559 # Remove circular references so that do_a[recrdeptask] = "do_a do_b" can work
560 for task in recursivetasks:
561 extradeps[task].difference_update(recursivetasksselfref)
562
563 for task in xrange(len(taskData.tasks_name)):
564 # Add in extra dependencies
565 if task in extradeps:
566 self.runq_depends[task] = extradeps[task]
567 # Remove all self references
568 if task in self.runq_depends[task]:
569 logger.debug(2, "Task %s (%s %s) contains self reference! %s", task, taskData.fn_index[taskData.tasks_fnid[task]], taskData.tasks_name[task], self.runq_depends[task])
570 self.runq_depends[task].remove(task)
571
572 # Step B - Mark all active tasks
573 #
574 # Start with the tasks we were asked to run and mark all dependencies
575 # as active too. If the task is to be 'forced', clear its stamp. Once
576 # all active tasks are marked, prune the ones we don't need.
577
578 logger.verbose("Marking Active Tasks")
579
580 def mark_active(listid, depth):
581 """
582 Mark an item as active along with its depends
583 (calls itself recursively)
584 """
585
586 if runq_build[listid] == 1:
587 return
588
589 runq_build[listid] = 1
590
591 depends = self.runq_depends[listid]
592 for depend in depends:
593 mark_active(depend, depth+1)
594
595 self.target_pairs = []
596 for target in self.targets:
597 targetid = taskData.getbuild_id(target[0])
598
599 if targetid not in taskData.build_targets:
600 continue
601
602 if targetid in taskData.failed_deps:
603 continue
604
605 fnid = taskData.build_targets[targetid][0]
606 fn = taskData.fn_index[fnid]
607 self.target_pairs.append((fn, target[1]))
608
609 if fnid in taskData.failed_fnids:
610 continue
611
612 if target[1] not in taskData.tasks_lookup[fnid]:
613 import difflib
614 close_matches = difflib.get_close_matches(target[1], taskData.tasks_lookup[fnid], cutoff=0.7)
615 if close_matches:
616 extra = ". Close matches:\n %s" % "\n ".join(close_matches)
617 else:
618 extra = ""
619 bb.msg.fatal("RunQueue", "Task %s does not exist for target %s%s" % (target[1], target[0], extra))
620
621 listid = taskData.tasks_lookup[fnid][target[1]]
622
623 mark_active(listid, 1)
624
625 # Step C - Prune all inactive tasks
626 #
627 # Once all active tasks are marked, prune the ones we don't need.
628
629 maps = []
630 delcount = 0
631 for listid in xrange(len(self.runq_fnid)):
632 if runq_build[listid-delcount] == 1:
633 maps.append(listid-delcount)
634 else:
635 del self.runq_fnid[listid-delcount]
636 del self.runq_task[listid-delcount]
637 del self.runq_depends[listid-delcount]
638 del runq_build[listid-delcount]
639 del self.runq_revdeps[listid-delcount]
640 del self.runq_hash[listid-delcount]
641 delcount = delcount + 1
642 maps.append(-1)
643
644 #
645 # Step D - Sanity checks and computation
646 #
647
648 # Check to make sure we still have tasks to run
649 if len(self.runq_fnid) == 0:
650 if not taskData.abort:
651 bb.msg.fatal("RunQueue", "All buildable tasks have been run but the build is incomplete (--continue mode). Errors for the tasks that failed will have been printed above.")
652 else:
653 bb.msg.fatal("RunQueue", "No active tasks and not in --continue mode?! Please report this bug.")
654
655 logger.verbose("Pruned %s inactive tasks, %s left", delcount, len(self.runq_fnid))
656
657 # Remap the dependencies to account for the deleted tasks
658 # Check we didn't delete a task we depend on
659 for listid in xrange(len(self.runq_fnid)):
660 newdeps = []
661 origdeps = self.runq_depends[listid]
662 for origdep in origdeps:
663 if maps[origdep] == -1:
664 bb.msg.fatal("RunQueue", "Invalid mapping - Should never happen!")
665 newdeps.append(maps[origdep])
666 self.runq_depends[listid] = set(newdeps)
667
668 logger.verbose("Assign Weightings")
669
670 # Generate a list of reverse dependencies to ease future calculations
671 for listid in xrange(len(self.runq_fnid)):
672 for dep in self.runq_depends[listid]:
673 self.runq_revdeps[dep].add(listid)
674
675 # Identify tasks at the end of dependency chains
676 # Error on circular dependency loops (length two)
677 endpoints = []
678 for listid in xrange(len(self.runq_fnid)):
679 revdeps = self.runq_revdeps[listid]
680 if len(revdeps) == 0:
681 endpoints.append(listid)
682 for dep in revdeps:
683 if dep in self.runq_depends[listid]:
684 #self.dump_data(taskData)
685 bb.msg.fatal("RunQueue", "Task %s (%s) has circular dependency on %s (%s)" % (taskData.fn_index[self.runq_fnid[dep]], self.runq_task[dep], taskData.fn_index[self.runq_fnid[listid]], self.runq_task[listid]))
686
687 logger.verbose("Compute totals (have %s endpoint(s))", len(endpoints))
688
689 # Calculate task weights
690 # Check of higher length circular dependencies
691 self.runq_weight = self.calculate_task_weights(endpoints)
692
693 # Sanity Check - Check for multiple tasks building the same provider
694 prov_list = {}
695 seen_fn = []
696 for task in xrange(len(self.runq_fnid)):
697 fn = taskData.fn_index[self.runq_fnid[task]]
698 if fn in seen_fn:
699 continue
700 seen_fn.append(fn)
701 for prov in self.dataCache.fn_provides[fn]:
702 if prov not in prov_list:
703 prov_list[prov] = [fn]
704 elif fn not in prov_list[prov]:
705 prov_list[prov].append(fn)
706 for prov in prov_list:
707 if len(prov_list[prov]) > 1 and prov not in self.multi_provider_whitelist:
708 seen_pn = []
709 # If two versions of the same PN are being built its fatal, we don't support it.
710 for fn in prov_list[prov]:
711 pn = self.dataCache.pkg_fn[fn]
712 if pn not in seen_pn:
713 seen_pn.append(pn)
714 else:
715 bb.fatal("Multiple versions of %s are due to be built (%s). Only one version of a given PN should be built in any given build. You likely need to set PREFERRED_VERSION_%s to select the correct version or don't depend on multiple versions." % (pn, " ".join(prov_list[prov]), pn))
716 msg = "Multiple .bb files are due to be built which each provide %s (%s)." % (prov, " ".join(prov_list[prov]))
717 if self.warn_multi_bb:
718 logger.warn(msg)
719 else:
720 msg += "\n This usually means one provides something the other doesn't and should."
721 logger.error(msg)
722
723 # Create a whitelist usable by the stamp checks
724 stampfnwhitelist = []
725 for entry in self.stampwhitelist.split():
726 entryid = self.taskData.getbuild_id(entry)
727 if entryid not in self.taskData.build_targets:
728 continue
729 fnid = self.taskData.build_targets[entryid][0]
730 fn = self.taskData.fn_index[fnid]
731 stampfnwhitelist.append(fn)
732 self.stampfnwhitelist = stampfnwhitelist
733
734 # Iterate over the task list looking for tasks with a 'setscene' function
735 self.runq_setscene = []
736 if not self.cooker.configuration.nosetscene:
737 for task in range(len(self.runq_fnid)):
738 setscene = taskData.gettask_id(self.taskData.fn_index[self.runq_fnid[task]], self.runq_task[task] + "_setscene", False)
739 if not setscene:
740 continue
741 self.runq_setscene.append(task)
742
743 def invalidate_task(fn, taskname, error_nostamp):
744 taskdep = self.dataCache.task_deps[fn]
745 fnid = self.taskData.getfn_id(fn)
746 if taskname not in taskData.tasks_lookup[fnid]:
747 logger.warn("Task %s does not exist, invalidating this task will have no effect" % taskname)
748 if 'nostamp' in taskdep and taskname in taskdep['nostamp']:
749 if error_nostamp:
750 bb.fatal("Task %s is marked nostamp, cannot invalidate this task" % taskname)
751 else:
752 bb.debug(1, "Task %s is marked nostamp, cannot invalidate this task" % taskname)
753 else:
754 logger.verbose("Invalidate task %s, %s", taskname, fn)
755 bb.parse.siggen.invalidate_task(taskname, self.dataCache, fn)
756
757 # Invalidate task if force mode active
758 if self.cooker.configuration.force:
759 for (fn, target) in self.target_pairs:
760 invalidate_task(fn, target, False)
761
762 # Invalidate task if invalidate mode active
763 if self.cooker.configuration.invalidate_stamp:
764 for (fn, target) in self.target_pairs:
765 for st in self.cooker.configuration.invalidate_stamp.split(','):
766 invalidate_task(fn, "do_%s" % st, True)
767
768 # Interate over the task list and call into the siggen code
769 dealtwith = set()
770 todeal = set(range(len(self.runq_fnid)))
771 while len(todeal) > 0:
772 for task in todeal.copy():
773 if len(self.runq_depends[task] - dealtwith) == 0:
774 dealtwith.add(task)
775 todeal.remove(task)
776 procdep = []
777 for dep in self.runq_depends[task]:
778 procdep.append(self.taskData.fn_index[self.runq_fnid[dep]] + "." + self.runq_task[dep])
779 self.runq_hash[task] = bb.parse.siggen.get_taskhash(self.taskData.fn_index[self.runq_fnid[task]], self.runq_task[task], procdep, self.dataCache)
780
781 self.hashes = {}
782 self.hash_deps = {}
783 for task in xrange(len(self.runq_fnid)):
784 identifier = '%s.%s' % (self.taskData.fn_index[self.runq_fnid[task]],
785 self.runq_task[task])
786 self.hashes[identifier] = self.runq_hash[task]
787 deps = []
788 for dep in self.runq_depends[task]:
789 depidentifier = '%s.%s' % (self.taskData.fn_index[self.runq_fnid[dep]],
790 self.runq_task[dep])
791 deps.append(depidentifier)
792 self.hash_deps[identifier] = deps
793
794 return len(self.runq_fnid)
795
796 def dump_data(self, taskQueue):
797 """
798 Dump some debug information on the internal data structures
799 """
800 logger.debug(3, "run_tasks:")
801 for task in xrange(len(self.rqdata.runq_task)):
802 logger.debug(3, " (%s)%s - %s: %s Deps %s RevDeps %s", task,
803 taskQueue.fn_index[self.rqdata.runq_fnid[task]],
804 self.rqdata.runq_task[task],
805 self.rqdata.runq_weight[task],
806 self.rqdata.runq_depends[task],
807 self.rqdata.runq_revdeps[task])
808
809 logger.debug(3, "sorted_tasks:")
810 for task1 in xrange(len(self.rqdata.runq_task)):
811 if task1 in self.prio_map:
812 task = self.prio_map[task1]
813 logger.debug(3, " (%s)%s - %s: %s Deps %s RevDeps %s", task,
814 taskQueue.fn_index[self.rqdata.runq_fnid[task]],
815 self.rqdata.runq_task[task],
816 self.rqdata.runq_weight[task],
817 self.rqdata.runq_depends[task],
818 self.rqdata.runq_revdeps[task])
819
820class RunQueue:
821 def __init__(self, cooker, cfgData, dataCache, taskData, targets):
822
823 self.cooker = cooker
824 self.cfgData = cfgData
825 self.rqdata = RunQueueData(self, cooker, cfgData, dataCache, taskData, targets)
826
827 self.stamppolicy = cfgData.getVar("BB_STAMP_POLICY", True) or "perfile"
828 self.hashvalidate = cfgData.getVar("BB_HASHCHECK_FUNCTION", True) or None
829 self.setsceneverify = cfgData.getVar("BB_SETSCENE_VERIFY_FUNCTION", True) or None
830 self.depvalidate = cfgData.getVar("BB_SETSCENE_DEPVALID", True) or None
831
832 self.state = runQueuePrepare
833
834 # For disk space monitor
835 self.dm = monitordisk.diskMonitor(cfgData)
836
837 self.rqexe = None
838 self.worker = None
839 self.workerpipe = None
840 self.fakeworker = None
841 self.fakeworkerpipe = None
842
843 def _start_worker(self, fakeroot = False, rqexec = None):
844 logger.debug(1, "Starting bitbake-worker")
845 if fakeroot:
846 fakerootcmd = self.cfgData.getVar("FAKEROOTCMD", True)
847 fakerootenv = (self.cfgData.getVar("FAKEROOTBASEENV", True) or "").split()
848 env = os.environ.copy()
849 for key, value in (var.split('=') for var in fakerootenv):
850 env[key] = value
851 worker = subprocess.Popen([fakerootcmd, "bitbake-worker", "decafbad"], stdout=subprocess.PIPE, stdin=subprocess.PIPE, env=env)
852 else:
853 worker = subprocess.Popen(["bitbake-worker", "decafbad"], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
854 bb.utils.nonblockingfd(worker.stdout)
855 workerpipe = runQueuePipe(worker.stdout, None, self.cfgData, rqexec)
856
857 workerdata = {
858 "taskdeps" : self.rqdata.dataCache.task_deps,
859 "fakerootenv" : self.rqdata.dataCache.fakerootenv,
860 "fakerootdirs" : self.rqdata.dataCache.fakerootdirs,
861 "fakerootnoenv" : self.rqdata.dataCache.fakerootnoenv,
862 "hashes" : self.rqdata.hashes,
863 "hash_deps" : self.rqdata.hash_deps,
864 "sigchecksums" : bb.parse.siggen.file_checksum_values,
865 "runq_hash" : self.rqdata.runq_hash,
866 "logdefaultdebug" : bb.msg.loggerDefaultDebugLevel,
867 "logdefaultverbose" : bb.msg.loggerDefaultVerbose,
868 "logdefaultverboselogs" : bb.msg.loggerVerboseLogs,
869 "logdefaultdomain" : bb.msg.loggerDefaultDomains,
870 "prhost" : self.cooker.prhost,
871 "buildname" : self.cfgData.getVar("BUILDNAME", True),
872 "date" : self.cfgData.getVar("DATE", True),
873 "time" : self.cfgData.getVar("TIME", True),
874 }
875
876 worker.stdin.write("<cookerconfig>" + pickle.dumps(self.cooker.configuration) + "</cookerconfig>")
877 worker.stdin.write("<workerdata>" + pickle.dumps(workerdata) + "</workerdata>")
878 worker.stdin.flush()
879
880 return worker, workerpipe
881
882 def _teardown_worker(self, worker, workerpipe):
883 if not worker:
884 return
885 logger.debug(1, "Teardown for bitbake-worker")
886 worker.stdin.write("<quit></quit>")
887 worker.stdin.flush()
888 while worker.returncode is None:
889 workerpipe.read()
890 worker.poll()
891 while workerpipe.read():
892 continue
893 workerpipe.close()
894
895 def start_worker(self):
896 if self.worker:
897 self.teardown_workers()
898 self.worker, self.workerpipe = self._start_worker()
899
900 def start_fakeworker(self, rqexec):
901 if not self.fakeworker:
902 self.fakeworker, self.fakeworkerpipe = self._start_worker(True, rqexec)
903
904 def teardown_workers(self):
905 self._teardown_worker(self.worker, self.workerpipe)
906 self.worker = None
907 self.workerpipe = None
908 self._teardown_worker(self.fakeworker, self.fakeworkerpipe)
909 self.fakeworker = None
910 self.fakeworkerpipe = None
911
912 def read_workers(self):
913 self.workerpipe.read()
914 if self.fakeworkerpipe:
915 self.fakeworkerpipe.read()
916
917 def active_fds(self):
918 fds = []
919 if self.workerpipe:
920 fds.append(self.workerpipe.input)
921 if self.fakeworkerpipe:
922 fds.append(self.fakeworkerpipe.input)
923 return fds
924
925 def check_stamp_task(self, task, taskname = None, recurse = False, cache = None):
926 def get_timestamp(f):
927 try:
928 if not os.access(f, os.F_OK):
929 return None
930 return os.stat(f)[stat.ST_MTIME]
931 except:
932 return None
933
934 if self.stamppolicy == "perfile":
935 fulldeptree = False
936 else:
937 fulldeptree = True
938 stampwhitelist = []
939 if self.stamppolicy == "whitelist":
940 stampwhitelist = self.rqdata.stampfnwhitelist
941
942 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]]
943 if taskname is None:
944 taskname = self.rqdata.runq_task[task]
945
946 stampfile = bb.build.stampfile(taskname, self.rqdata.dataCache, fn)
947
948 # If the stamp is missing its not current
949 if not os.access(stampfile, os.F_OK):
950 logger.debug(2, "Stampfile %s not available", stampfile)
951 return False
952 # If its a 'nostamp' task, it's not current
953 taskdep = self.rqdata.dataCache.task_deps[fn]
954 if 'nostamp' in taskdep and taskname in taskdep['nostamp']:
955 logger.debug(2, "%s.%s is nostamp\n", fn, taskname)
956 return False
957
958 if taskname != "do_setscene" and taskname.endswith("_setscene"):
959 return True
960
961 if cache is None:
962 cache = {}
963
964 iscurrent = True
965 t1 = get_timestamp(stampfile)
966 for dep in self.rqdata.runq_depends[task]:
967 if iscurrent:
968 fn2 = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[dep]]
969 taskname2 = self.rqdata.runq_task[dep]
970 stampfile2 = bb.build.stampfile(taskname2, self.rqdata.dataCache, fn2)
971 stampfile3 = bb.build.stampfile(taskname2 + "_setscene", self.rqdata.dataCache, fn2)
972 t2 = get_timestamp(stampfile2)
973 t3 = get_timestamp(stampfile3)
974 if t3 and t3 > t2:
975 continue
976 if fn == fn2 or (fulldeptree and fn2 not in stampwhitelist):
977 if not t2:
978 logger.debug(2, 'Stampfile %s does not exist', stampfile2)
979 iscurrent = False
980 if t1 < t2:
981 logger.debug(2, 'Stampfile %s < %s', stampfile, stampfile2)
982 iscurrent = False
983 if recurse and iscurrent:
984 if dep in cache:
985 iscurrent = cache[dep]
986 if not iscurrent:
987 logger.debug(2, 'Stampfile for dependency %s:%s invalid (cached)' % (fn2, taskname2))
988 else:
989 iscurrent = self.check_stamp_task(dep, recurse=True, cache=cache)
990 cache[dep] = iscurrent
991 if recurse:
992 cache[task] = iscurrent
993 return iscurrent
994
995 def _execute_runqueue(self):
996 """
997 Run the tasks in a queue prepared by rqdata.prepare()
998 Upon failure, optionally try to recover the build using any alternate providers
999 (if the abort on failure configuration option isn't set)
1000 """
1001
1002 retval = True
1003
1004 if self.state is runQueuePrepare:
1005 self.rqexe = RunQueueExecuteDummy(self)
1006 if self.rqdata.prepare() == 0:
1007 self.state = runQueueComplete
1008 else:
1009 self.state = runQueueSceneInit
1010
1011 # we are ready to run, see if any UI client needs the dependency info
1012 if bb.cooker.CookerFeatures.SEND_DEPENDS_TREE in self.cooker.featureset:
1013 depgraph = self.cooker.buildDependTree(self, self.rqdata.taskData)
1014 bb.event.fire(bb.event.DepTreeGenerated(depgraph), self.cooker.data)
1015
1016 if self.state is runQueueSceneInit:
1017 if self.cooker.configuration.dump_signatures:
1018 self.dump_signatures()
1019 else:
1020 self.start_worker()
1021 self.rqexe = RunQueueExecuteScenequeue(self)
1022
1023 if self.state in [runQueueSceneRun, runQueueRunning, runQueueCleanUp]:
1024 self.dm.check(self)
1025
1026 if self.state is runQueueSceneRun:
1027 retval = self.rqexe.execute()
1028
1029 if self.state is runQueueRunInit:
1030 logger.info("Executing RunQueue Tasks")
1031 self.rqexe = RunQueueExecuteTasks(self)
1032 self.state = runQueueRunning
1033
1034 if self.state is runQueueRunning:
1035 retval = self.rqexe.execute()
1036
1037 if self.state is runQueueCleanUp:
1038 self.rqexe.finish()
1039
1040 if self.state is runQueueComplete or self.state is runQueueFailed:
1041 self.teardown_workers()
1042 if self.rqexe.stats.failed:
1043 logger.info("Tasks Summary: Attempted %d tasks of which %d didn't need to be rerun and %d failed.", self.rqexe.stats.completed + self.rqexe.stats.failed, self.rqexe.stats.skipped, self.rqexe.stats.failed)
1044 else:
1045 # Let's avoid the word "failed" if nothing actually did
1046 logger.info("Tasks Summary: Attempted %d tasks of which %d didn't need to be rerun and all succeeded.", self.rqexe.stats.completed, self.rqexe.stats.skipped)
1047
1048 if self.state is runQueueFailed:
1049 if not self.rqdata.taskData.tryaltconfigs:
1050 raise bb.runqueue.TaskFailure(self.rqexe.failed_fnids)
1051 for fnid in self.rqexe.failed_fnids:
1052 self.rqdata.taskData.fail_fnid(fnid)
1053 self.rqdata.reset()
1054
1055 if self.state is runQueueComplete:
1056 # All done
1057 return False
1058
1059 # Loop
1060 return retval
1061
1062 def execute_runqueue(self):
1063 # Catch unexpected exceptions and ensure we exit when an error occurs, not loop.
1064 try:
1065 return self._execute_runqueue()
1066 except bb.runqueue.TaskFailure:
1067 raise
1068 except SystemExit:
1069 raise
1070 except:
1071 logger.error("An uncaught exception occured in runqueue, please see the failure below:")
1072 try:
1073 self.teardown_workers()
1074 except:
1075 pass
1076 self.state = runQueueComplete
1077 raise
1078
1079 def finish_runqueue(self, now = False):
1080 if not self.rqexe:
1081 return
1082
1083 if now:
1084 self.rqexe.finish_now()
1085 else:
1086 self.rqexe.finish()
1087
1088 def dump_signatures(self):
1089 self.state = runQueueComplete
1090 done = set()
1091 bb.note("Reparsing files to collect dependency data")
1092 for task in range(len(self.rqdata.runq_fnid)):
1093 if self.rqdata.runq_fnid[task] not in done:
1094 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]]
1095 the_data = bb.cache.Cache.loadDataFull(fn, self.cooker.collection.get_file_appends(fn), self.cooker.data)
1096 done.add(self.rqdata.runq_fnid[task])
1097
1098 bb.parse.siggen.dump_sigs(self.rqdata.dataCache)
1099
1100 return
1101
1102
1103class RunQueueExecute:
1104
1105 def __init__(self, rq):
1106 self.rq = rq
1107 self.cooker = rq.cooker
1108 self.cfgData = rq.cfgData
1109 self.rqdata = rq.rqdata
1110
1111 self.number_tasks = int(self.cfgData.getVar("BB_NUMBER_THREADS", True) or 1)
1112 self.scheduler = self.cfgData.getVar("BB_SCHEDULER", True) or "speed"
1113
1114 self.runq_buildable = []
1115 self.runq_running = []
1116 self.runq_complete = []
1117
1118 self.build_stamps = {}
1119 self.failed_fnids = []
1120
1121 self.stampcache = {}
1122
1123 rq.workerpipe.setrunqueueexec(self)
1124 if rq.fakeworkerpipe:
1125 rq.fakeworkerpipe.setrunqueueexec(self)
1126
1127 def runqueue_process_waitpid(self, task, status):
1128
1129 # self.build_stamps[pid] may not exist when use shared work directory.
1130 if task in self.build_stamps:
1131 del self.build_stamps[task]
1132
1133 if status != 0:
1134 self.task_fail(task, status)
1135 else:
1136 self.task_complete(task)
1137 return True
1138
1139 def finish_now(self):
1140
1141 self.rq.worker.stdin.write("<finishnow></finishnow>")
1142 self.rq.worker.stdin.flush()
1143 if self.rq.fakeworker:
1144 self.rq.fakeworker.stdin.write("<finishnow></finishnow>")
1145 self.rq.fakeworker.stdin.flush()
1146
1147 if len(self.failed_fnids) != 0:
1148 self.rq.state = runQueueFailed
1149 return
1150
1151 self.rq.state = runQueueComplete
1152 return
1153
1154 def finish(self):
1155 self.rq.state = runQueueCleanUp
1156
1157 if self.stats.active > 0:
1158 bb.event.fire(runQueueExitWait(self.stats.active), self.cfgData)
1159 self.rq.read_workers()
1160
1161 return
1162
1163 if len(self.failed_fnids) != 0:
1164 self.rq.state = runQueueFailed
1165 return
1166
1167 self.rq.state = runQueueComplete
1168 return
1169
1170 def check_dependencies(self, task, taskdeps, setscene = False):
1171 if not self.rq.depvalidate:
1172 return False
1173
1174 taskdata = {}
1175 taskdeps.add(task)
1176 for dep in taskdeps:
1177 if setscene:
1178 depid = self.rqdata.runq_setscene[dep]
1179 else:
1180 depid = dep
1181 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[depid]]
1182 pn = self.rqdata.dataCache.pkg_fn[fn]
1183 taskname = self.rqdata.runq_task[depid]
1184 taskdata[dep] = [pn, taskname, fn]
1185 call = self.rq.depvalidate + "(task, taskdata, notneeded, d)"
1186 locs = { "task" : task, "taskdata" : taskdata, "notneeded" : self.scenequeue_notneeded, "d" : self.cooker.data }
1187 valid = bb.utils.better_eval(call, locs)
1188 return valid
1189
1190class RunQueueExecuteDummy(RunQueueExecute):
1191 def __init__(self, rq):
1192 self.rq = rq
1193 self.stats = RunQueueStats(0)
1194
1195 def finish(self):
1196 self.rq.state = runQueueComplete
1197 return
1198
1199class RunQueueExecuteTasks(RunQueueExecute):
1200 def __init__(self, rq):
1201 RunQueueExecute.__init__(self, rq)
1202
1203 self.stats = RunQueueStats(len(self.rqdata.runq_fnid))
1204
1205 self.stampcache = {}
1206
1207 # Mark initial buildable tasks
1208 for task in xrange(self.stats.total):
1209 self.runq_running.append(0)
1210 self.runq_complete.append(0)
1211 if len(self.rqdata.runq_depends[task]) == 0:
1212 self.runq_buildable.append(1)
1213 else:
1214 self.runq_buildable.append(0)
1215 if len(self.rqdata.runq_revdeps[task]) > 0 and self.rqdata.runq_revdeps[task].issubset(self.rq.scenequeue_covered) and task not in self.rq.scenequeue_notcovered:
1216 self.rq.scenequeue_covered.add(task)
1217
1218 found = True
1219 while found:
1220 found = False
1221 for task in xrange(self.stats.total):
1222 if task in self.rq.scenequeue_covered:
1223 continue
1224 logger.debug(1, 'Considering %s (%s): %s' % (task, self.rqdata.get_user_idstring(task), str(self.rqdata.runq_revdeps[task])))
1225
1226 if len(self.rqdata.runq_revdeps[task]) > 0 and self.rqdata.runq_revdeps[task].issubset(self.rq.scenequeue_covered) and task not in self.rq.scenequeue_notcovered:
1227 found = True
1228 self.rq.scenequeue_covered.add(task)
1229
1230 logger.debug(1, 'Skip list (pre setsceneverify) %s', sorted(self.rq.scenequeue_covered))
1231
1232 # Allow the metadata to elect for setscene tasks to run anyway
1233 covered_remove = set()
1234 if self.rq.setsceneverify:
1235 invalidtasks = []
1236 for task in xrange(len(self.rqdata.runq_task)):
1237 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]]
1238 taskname = self.rqdata.runq_task[task]
1239 taskdep = self.rqdata.dataCache.task_deps[fn]
1240
1241 if 'noexec' in taskdep and taskname in taskdep['noexec']:
1242 continue
1243 if self.rq.check_stamp_task(task, taskname + "_setscene", cache=self.stampcache):
1244 logger.debug(2, 'Setscene stamp current for task %s(%s)', task, self.rqdata.get_user_idstring(task))
1245 continue
1246 if self.rq.check_stamp_task(task, taskname, recurse = True, cache=self.stampcache):
1247 logger.debug(2, 'Normal stamp current for task %s(%s)', task, self.rqdata.get_user_idstring(task))
1248 continue
1249 invalidtasks.append(task)
1250
1251 call = self.rq.setsceneverify + "(covered, tasknames, fnids, fns, d, invalidtasks=invalidtasks)"
1252 call2 = self.rq.setsceneverify + "(covered, tasknames, fnids, fns, d)"
1253 locs = { "covered" : self.rq.scenequeue_covered, "tasknames" : self.rqdata.runq_task, "fnids" : self.rqdata.runq_fnid, "fns" : self.rqdata.taskData.fn_index, "d" : self.cooker.data, "invalidtasks" : invalidtasks }
1254 # Backwards compatibility with older versions without invalidtasks
1255 try:
1256 covered_remove = bb.utils.better_eval(call, locs)
1257 except TypeError:
1258 covered_remove = bb.utils.better_eval(call2, locs)
1259
1260 for task in covered_remove:
1261 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]]
1262 taskname = self.rqdata.runq_task[task] + '_setscene'
1263 bb.build.del_stamp(taskname, self.rqdata.dataCache, fn)
1264 logger.debug(1, 'Not skipping task %s due to setsceneverify', task)
1265 self.rq.scenequeue_covered.remove(task)
1266
1267 logger.debug(1, 'Full skip list %s', self.rq.scenequeue_covered)
1268
1269 event.fire(bb.event.StampUpdate(self.rqdata.target_pairs, self.rqdata.dataCache.stamp), self.cfgData)
1270
1271 schedulers = self.get_schedulers()
1272 for scheduler in schedulers:
1273 if self.scheduler == scheduler.name:
1274 self.sched = scheduler(self, self.rqdata)
1275 logger.debug(1, "Using runqueue scheduler '%s'", scheduler.name)
1276 break
1277 else:
1278 bb.fatal("Invalid scheduler '%s'. Available schedulers: %s" %
1279 (self.scheduler, ", ".join(obj.name for obj in schedulers)))
1280
1281 def get_schedulers(self):
1282 schedulers = set(obj for obj in globals().values()
1283 if type(obj) is type and
1284 issubclass(obj, RunQueueScheduler))
1285
1286 user_schedulers = self.cfgData.getVar("BB_SCHEDULERS", True)
1287 if user_schedulers:
1288 for sched in user_schedulers.split():
1289 if not "." in sched:
1290 bb.note("Ignoring scheduler '%s' from BB_SCHEDULERS: not an import" % sched)
1291 continue
1292
1293 modname, name = sched.rsplit(".", 1)
1294 try:
1295 module = __import__(modname, fromlist=(name,))
1296 except ImportError as exc:
1297 logger.critical("Unable to import scheduler '%s' from '%s': %s" % (name, modname, exc))
1298 raise SystemExit(1)
1299 else:
1300 schedulers.add(getattr(module, name))
1301 return schedulers
1302
1303 def task_completeoutright(self, task):
1304 """
1305 Mark a task as completed
1306 Look at the reverse dependencies and mark any task with
1307 completed dependencies as buildable
1308 """
1309 self.runq_complete[task] = 1
1310 for revdep in self.rqdata.runq_revdeps[task]:
1311 if self.runq_running[revdep] == 1:
1312 continue
1313 if self.runq_buildable[revdep] == 1:
1314 continue
1315 alldeps = 1
1316 for dep in self.rqdata.runq_depends[revdep]:
1317 if self.runq_complete[dep] != 1:
1318 alldeps = 0
1319 if alldeps == 1:
1320 self.runq_buildable[revdep] = 1
1321 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[revdep]]
1322 taskname = self.rqdata.runq_task[revdep]
1323 logger.debug(1, "Marking task %s (%s, %s) as buildable", revdep, fn, taskname)
1324
1325 def task_complete(self, task):
1326 self.stats.taskCompleted()
1327 bb.event.fire(runQueueTaskCompleted(task, self.stats, self.rq), self.cfgData)
1328 self.task_completeoutright(task)
1329
1330 def task_fail(self, task, exitcode):
1331 """
1332 Called when a task has failed
1333 Updates the state engine with the failure
1334 """
1335 self.stats.taskFailed()
1336 fnid = self.rqdata.runq_fnid[task]
1337 self.failed_fnids.append(fnid)
1338 bb.event.fire(runQueueTaskFailed(task, self.stats, exitcode, self.rq), self.cfgData)
1339 if self.rqdata.taskData.abort:
1340 self.rq.state = runQueueCleanUp
1341
1342 def task_skip(self, task, reason):
1343 self.runq_running[task] = 1
1344 self.runq_buildable[task] = 1
1345 bb.event.fire(runQueueTaskSkipped(task, self.stats, self.rq, reason), self.cfgData)
1346 self.task_completeoutright(task)
1347 self.stats.taskCompleted()
1348 self.stats.taskSkipped()
1349
1350 def execute(self):
1351 """
1352 Run the tasks in a queue prepared by rqdata.prepare()
1353 """
1354
1355 self.rq.read_workers()
1356
1357
1358 if self.stats.total == 0:
1359 # nothing to do
1360 self.rq.state = runQueueCleanUp
1361
1362 task = self.sched.next()
1363 if task is not None:
1364 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]]
1365 taskname = self.rqdata.runq_task[task]
1366
1367 if task in self.rq.scenequeue_covered:
1368 logger.debug(2, "Setscene covered task %s (%s)", task,
1369 self.rqdata.get_user_idstring(task))
1370 self.task_skip(task, "covered")
1371 return True
1372
1373 if self.rq.check_stamp_task(task, taskname, cache=self.stampcache):
1374 logger.debug(2, "Stamp current task %s (%s)", task,
1375 self.rqdata.get_user_idstring(task))
1376 self.task_skip(task, "existing")
1377 return True
1378
1379 taskdep = self.rqdata.dataCache.task_deps[fn]
1380 if 'noexec' in taskdep and taskname in taskdep['noexec']:
1381 startevent = runQueueTaskStarted(task, self.stats, self.rq,
1382 noexec=True)
1383 bb.event.fire(startevent, self.cfgData)
1384 self.runq_running[task] = 1
1385 self.stats.taskActive()
1386 bb.build.make_stamp(taskname, self.rqdata.dataCache, fn)
1387 self.task_complete(task)
1388 return True
1389 else:
1390 startevent = runQueueTaskStarted(task, self.stats, self.rq)
1391 bb.event.fire(startevent, self.cfgData)
1392
1393 taskdep = self.rqdata.dataCache.task_deps[fn]
1394 if 'fakeroot' in taskdep and taskname in taskdep['fakeroot']:
1395 if not self.rq.fakeworker:
1396 self.rq.start_fakeworker(self)
1397 self.rq.fakeworker.stdin.write("<runtask>" + pickle.dumps((fn, task, taskname, False, self.cooker.collection.get_file_appends(fn))) + "</runtask>")
1398 self.rq.fakeworker.stdin.flush()
1399 else:
1400 self.rq.worker.stdin.write("<runtask>" + pickle.dumps((fn, task, taskname, False, self.cooker.collection.get_file_appends(fn))) + "</runtask>")
1401 self.rq.worker.stdin.flush()
1402
1403 self.build_stamps[task] = bb.build.stampfile(taskname, self.rqdata.dataCache, fn)
1404 self.runq_running[task] = 1
1405 self.stats.taskActive()
1406 if self.stats.active < self.number_tasks:
1407 return True
1408
1409 if self.stats.active > 0:
1410 self.rq.read_workers()
1411 return self.rq.active_fds()
1412
1413 if len(self.failed_fnids) != 0:
1414 self.rq.state = runQueueFailed
1415 return True
1416
1417 # Sanity Checks
1418 for task in xrange(self.stats.total):
1419 if self.runq_buildable[task] == 0:
1420 logger.error("Task %s never buildable!", task)
1421 if self.runq_running[task] == 0:
1422 logger.error("Task %s never ran!", task)
1423 if self.runq_complete[task] == 0:
1424 logger.error("Task %s never completed!", task)
1425 self.rq.state = runQueueComplete
1426
1427 return True
1428
1429class RunQueueExecuteScenequeue(RunQueueExecute):
1430 def __init__(self, rq):
1431 RunQueueExecute.__init__(self, rq)
1432
1433 self.scenequeue_covered = set()
1434 self.scenequeue_notcovered = set()
1435 self.scenequeue_notneeded = set()
1436
1437 # If we don't have any setscene functions, skip this step
1438 if len(self.rqdata.runq_setscene) == 0:
1439 rq.scenequeue_covered = set()
1440 rq.state = runQueueRunInit
1441 return
1442
1443 self.stats = RunQueueStats(len(self.rqdata.runq_setscene))
1444
1445 sq_revdeps = []
1446 sq_revdeps_new = []
1447 sq_revdeps_squash = []
1448 self.sq_harddeps = []
1449
1450 # We need to construct a dependency graph for the setscene functions. Intermediate
1451 # dependencies between the setscene tasks only complicate the code. This code
1452 # therefore aims to collapse the huge runqueue dependency tree into a smaller one
1453 # only containing the setscene functions.
1454
1455 for task in xrange(self.stats.total):
1456 self.runq_running.append(0)
1457 self.runq_complete.append(0)
1458 self.runq_buildable.append(0)
1459
1460 # First process the chains up to the first setscene task.
1461 endpoints = {}
1462 for task in xrange(len(self.rqdata.runq_fnid)):
1463 sq_revdeps.append(copy.copy(self.rqdata.runq_revdeps[task]))
1464 sq_revdeps_new.append(set())
1465 if (len(self.rqdata.runq_revdeps[task]) == 0) and task not in self.rqdata.runq_setscene:
1466 endpoints[task] = set()
1467
1468 # Secondly process the chains between setscene tasks.
1469 for task in self.rqdata.runq_setscene:
1470 for dep in self.rqdata.runq_depends[task]:
1471 if dep not in endpoints:
1472 endpoints[dep] = set()
1473 endpoints[dep].add(task)
1474
1475 def process_endpoints(endpoints):
1476 newendpoints = {}
1477 for point, task in endpoints.items():
1478 tasks = set()
1479 if task:
1480 tasks |= task
1481 if sq_revdeps_new[point]:
1482 tasks |= sq_revdeps_new[point]
1483 sq_revdeps_new[point] = set()
1484 if point in self.rqdata.runq_setscene:
1485 sq_revdeps_new[point] = tasks
1486 for dep in self.rqdata.runq_depends[point]:
1487 if point in sq_revdeps[dep]:
1488 sq_revdeps[dep].remove(point)
1489 if tasks:
1490 sq_revdeps_new[dep] |= tasks
1491 if (len(sq_revdeps[dep]) == 0 or len(sq_revdeps_new[dep]) != 0) and dep not in self.rqdata.runq_setscene:
1492 newendpoints[dep] = task
1493 if len(newendpoints) != 0:
1494 process_endpoints(newendpoints)
1495
1496 process_endpoints(endpoints)
1497
1498 # Build a list of setscene tasks which as "unskippable"
1499 # These are direct endpoints referenced by the build
1500 endpoints2 = {}
1501 sq_revdeps2 = []
1502 sq_revdeps_new2 = []
1503 def process_endpoints2(endpoints):
1504 newendpoints = {}
1505 for point, task in endpoints.items():
1506 tasks = set([point])
1507 if task:
1508 tasks |= task
1509 if sq_revdeps_new2[point]:
1510 tasks |= sq_revdeps_new2[point]
1511 sq_revdeps_new2[point] = set()
1512 if point in self.rqdata.runq_setscene:
1513 sq_revdeps_new2[point] = tasks
1514 for dep in self.rqdata.runq_depends[point]:
1515 if point in sq_revdeps2[dep]:
1516 sq_revdeps2[dep].remove(point)
1517 if tasks:
1518 sq_revdeps_new2[dep] |= tasks
1519 if (len(sq_revdeps2[dep]) == 0 or len(sq_revdeps_new2[dep]) != 0) and dep not in self.rqdata.runq_setscene:
1520 newendpoints[dep] = tasks
1521 if len(newendpoints) != 0:
1522 process_endpoints2(newendpoints)
1523 for task in xrange(len(self.rqdata.runq_fnid)):
1524 sq_revdeps2.append(copy.copy(self.rqdata.runq_revdeps[task]))
1525 sq_revdeps_new2.append(set())
1526 if (len(self.rqdata.runq_revdeps[task]) == 0) and task not in self.rqdata.runq_setscene:
1527 endpoints2[task] = set()
1528 process_endpoints2(endpoints2)
1529 self.unskippable = []
1530 for task in self.rqdata.runq_setscene:
1531 if sq_revdeps_new2[task]:
1532 self.unskippable.append(self.rqdata.runq_setscene.index(task))
1533
1534 for task in xrange(len(self.rqdata.runq_fnid)):
1535 if task in self.rqdata.runq_setscene:
1536 deps = set()
1537 for dep in sq_revdeps_new[task]:
1538 deps.add(self.rqdata.runq_setscene.index(dep))
1539 sq_revdeps_squash.append(deps)
1540 elif len(sq_revdeps_new[task]) != 0:
1541 bb.msg.fatal("RunQueue", "Something went badly wrong during scenequeue generation, aborting. Please report this problem.")
1542
1543 # Resolve setscene inter-task dependencies
1544 # e.g. do_sometask_setscene[depends] = "targetname:do_someothertask_setscene"
1545 # Note that anything explicitly depended upon will have its reverse dependencies removed to avoid circular dependencies
1546 for task in self.rqdata.runq_setscene:
1547 realid = self.rqdata.taskData.gettask_id(self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[task]], self.rqdata.runq_task[task] + "_setscene", False)
1548 idepends = self.rqdata.taskData.tasks_idepends[realid]
1549 for (depid, idependtask) in idepends:
1550 if depid not in self.rqdata.taskData.build_targets:
1551 continue
1552
1553 depdata = self.rqdata.taskData.build_targets[depid][0]
1554 if depdata is None:
1555 continue
1556 dep = self.rqdata.taskData.fn_index[depdata]
1557 taskid = self.rqdata.get_task_id(self.rqdata.taskData.getfn_id(dep), idependtask.replace("_setscene", ""))
1558 if taskid is None:
1559 bb.msg.fatal("RunQueue", "Task %s:%s depends upon non-existent task %s:%s" % (self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[realid]], self.rqdata.taskData.tasks_name[realid], dep, idependtask))
1560
1561 self.sq_harddeps.append(self.rqdata.runq_setscene.index(taskid))
1562 sq_revdeps_squash[self.rqdata.runq_setscene.index(task)].add(self.rqdata.runq_setscene.index(taskid))
1563 # Have to zero this to avoid circular dependencies
1564 sq_revdeps_squash[self.rqdata.runq_setscene.index(taskid)] = set()
1565
1566 #for task in xrange(len(sq_revdeps_squash)):
1567 # print "Task %s: %s.%s is %s " % (task, self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[self.rqdata.runq_setscene[task]]], self.rqdata.runq_task[self.rqdata.runq_setscene[task]] + "_setscene", sq_revdeps_squash[task])
1568
1569 self.sq_deps = []
1570 self.sq_revdeps = sq_revdeps_squash
1571 self.sq_revdeps2 = copy.deepcopy(self.sq_revdeps)
1572
1573 for task in xrange(len(self.sq_revdeps)):
1574 self.sq_deps.append(set())
1575 for task in xrange(len(self.sq_revdeps)):
1576 for dep in self.sq_revdeps[task]:
1577 self.sq_deps[dep].add(task)
1578
1579 for task in xrange(len(self.sq_revdeps)):
1580 if len(self.sq_revdeps[task]) == 0:
1581 self.runq_buildable[task] = 1
1582
1583 if self.rq.hashvalidate:
1584 sq_hash = []
1585 sq_hashfn = []
1586 sq_fn = []
1587 sq_taskname = []
1588 sq_task = []
1589 noexec = []
1590 stamppresent = []
1591 for task in xrange(len(self.sq_revdeps)):
1592 realtask = self.rqdata.runq_setscene[task]
1593 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[realtask]]
1594 taskname = self.rqdata.runq_task[realtask]
1595 taskdep = self.rqdata.dataCache.task_deps[fn]
1596
1597 if 'noexec' in taskdep and taskname in taskdep['noexec']:
1598 noexec.append(task)
1599 self.task_skip(task)
1600 bb.build.make_stamp(taskname + "_setscene", self.rqdata.dataCache, fn)
1601 continue
1602
1603 if self.rq.check_stamp_task(realtask, taskname + "_setscene", cache=self.stampcache):
1604 logger.debug(2, 'Setscene stamp current for task %s(%s)', task, self.rqdata.get_user_idstring(realtask))
1605 stamppresent.append(task)
1606 self.task_skip(task)
1607 continue
1608
1609 if self.rq.check_stamp_task(realtask, taskname, recurse = True, cache=self.stampcache):
1610 logger.debug(2, 'Normal stamp current for task %s(%s)', task, self.rqdata.get_user_idstring(realtask))
1611 stamppresent.append(task)
1612 self.task_skip(task)
1613 continue
1614
1615 sq_fn.append(fn)
1616 sq_hashfn.append(self.rqdata.dataCache.hashfn[fn])
1617 sq_hash.append(self.rqdata.runq_hash[realtask])
1618 sq_taskname.append(taskname)
1619 sq_task.append(task)
1620 call = self.rq.hashvalidate + "(sq_fn, sq_task, sq_hash, sq_hashfn, d)"
1621 locs = { "sq_fn" : sq_fn, "sq_task" : sq_taskname, "sq_hash" : sq_hash, "sq_hashfn" : sq_hashfn, "d" : self.cooker.data }
1622 valid = bb.utils.better_eval(call, locs)
1623
1624 valid_new = stamppresent
1625 for v in valid:
1626 valid_new.append(sq_task[v])
1627
1628 for task in xrange(len(self.sq_revdeps)):
1629 if task not in valid_new and task not in noexec:
1630 realtask = self.rqdata.runq_setscene[task]
1631 logger.debug(2, 'No package found, so skipping setscene task %s',
1632 self.rqdata.get_user_idstring(realtask))
1633 self.task_failoutright(task)
1634
1635 logger.info('Executing SetScene Tasks')
1636
1637 self.rq.state = runQueueSceneRun
1638
1639 def scenequeue_updatecounters(self, task, fail = False):
1640 for dep in self.sq_deps[task]:
1641 if fail and task in self.sq_harddeps:
1642 continue
1643 self.sq_revdeps2[dep].remove(task)
1644 if len(self.sq_revdeps2[dep]) == 0:
1645 self.runq_buildable[dep] = 1
1646
1647 def task_completeoutright(self, task):
1648 """
1649 Mark a task as completed
1650 Look at the reverse dependencies and mark any task with
1651 completed dependencies as buildable
1652 """
1653
1654 index = self.rqdata.runq_setscene[task]
1655 logger.debug(1, 'Found task %s which could be accelerated',
1656 self.rqdata.get_user_idstring(index))
1657
1658 self.scenequeue_covered.add(task)
1659 self.scenequeue_updatecounters(task)
1660
1661 def task_complete(self, task):
1662 self.stats.taskCompleted()
1663 bb.event.fire(sceneQueueTaskCompleted(task, self.stats, self.rq), self.cfgData)
1664 self.task_completeoutright(task)
1665
1666 def task_fail(self, task, result):
1667 self.stats.taskFailed()
1668 bb.event.fire(sceneQueueTaskFailed(task, self.stats, result, self), self.cfgData)
1669 self.scenequeue_notcovered.add(task)
1670 self.scenequeue_updatecounters(task, True)
1671
1672 def task_failoutright(self, task):
1673 self.runq_running[task] = 1
1674 self.runq_buildable[task] = 1
1675 self.stats.taskCompleted()
1676 self.stats.taskSkipped()
1677 index = self.rqdata.runq_setscene[task]
1678 self.scenequeue_notcovered.add(task)
1679 self.scenequeue_updatecounters(task, True)
1680
1681 def task_skip(self, task):
1682 self.runq_running[task] = 1
1683 self.runq_buildable[task] = 1
1684 self.task_completeoutright(task)
1685 self.stats.taskCompleted()
1686 self.stats.taskSkipped()
1687
1688 def execute(self):
1689 """
1690 Run the tasks in a queue prepared by prepare_runqueue
1691 """
1692
1693 self.rq.read_workers()
1694
1695 task = None
1696 if self.stats.active < self.number_tasks:
1697 # Find the next setscene to run
1698 for nexttask in xrange(self.stats.total):
1699 if self.runq_buildable[nexttask] == 1 and self.runq_running[nexttask] != 1:
1700 if nexttask in self.unskippable:
1701 logger.debug(2, "Setscene task %s is unskippable" % self.rqdata.get_user_idstring(self.rqdata.runq_setscene[nexttask]))
1702 if nexttask not in self.unskippable and len(self.sq_revdeps[nexttask]) > 0 and self.sq_revdeps[nexttask].issubset(self.scenequeue_covered) and self.check_dependencies(nexttask, self.sq_revdeps[nexttask], True):
1703 logger.debug(2, "Skipping setscene for task %s" % self.rqdata.get_user_idstring(self.rqdata.runq_setscene[nexttask]))
1704 self.task_skip(nexttask)
1705 self.scenequeue_notneeded.add(nexttask)
1706 return True
1707 task = nexttask
1708 break
1709 if task is not None:
1710 realtask = self.rqdata.runq_setscene[task]
1711 fn = self.rqdata.taskData.fn_index[self.rqdata.runq_fnid[realtask]]
1712
1713 taskname = self.rqdata.runq_task[realtask] + "_setscene"
1714 if self.rq.check_stamp_task(realtask, self.rqdata.runq_task[realtask], recurse = True, cache=self.stampcache):
1715 logger.debug(2, 'Stamp for underlying task %s(%s) is current, so skipping setscene variant',
1716 task, self.rqdata.get_user_idstring(realtask))
1717 self.task_failoutright(task)
1718 return True
1719
1720 if self.cooker.configuration.force:
1721 for target in self.rqdata.target_pairs:
1722 if target[0] == fn and target[1] == self.rqdata.runq_task[realtask]:
1723 self.task_failoutright(task)
1724 return True
1725
1726 if self.rq.check_stamp_task(realtask, taskname, cache=self.stampcache):
1727 logger.debug(2, 'Setscene stamp current task %s(%s), so skip it and its dependencies',
1728 task, self.rqdata.get_user_idstring(realtask))
1729 self.task_skip(task)
1730 return True
1731
1732 startevent = sceneQueueTaskStarted(task, self.stats, self.rq)
1733 bb.event.fire(startevent, self.cfgData)
1734
1735 taskdep = self.rqdata.dataCache.task_deps[fn]
1736 if 'fakeroot' in taskdep and taskname in taskdep['fakeroot']:
1737 if not self.rq.fakeworker:
1738 self.rq.start_fakeworker(self)
1739 self.rq.fakeworker.stdin.write("<runtask>" + pickle.dumps((fn, realtask, taskname, True, self.cooker.collection.get_file_appends(fn))) + "</runtask>")
1740 self.rq.fakeworker.stdin.flush()
1741 else:
1742 self.rq.worker.stdin.write("<runtask>" + pickle.dumps((fn, realtask, taskname, True, self.cooker.collection.get_file_appends(fn))) + "</runtask>")
1743 self.rq.worker.stdin.flush()
1744
1745 self.runq_running[task] = 1
1746 self.stats.taskActive()
1747 if self.stats.active < self.number_tasks:
1748 return True
1749
1750 if self.stats.active > 0:
1751 self.rq.read_workers()
1752 return self.rq.active_fds()
1753
1754 # Convert scenequeue_covered task numbers into full taskgraph ids
1755 oldcovered = self.scenequeue_covered
1756 self.rq.scenequeue_covered = set()
1757 for task in oldcovered:
1758 self.rq.scenequeue_covered.add(self.rqdata.runq_setscene[task])
1759 self.rq.scenequeue_notcovered = set()
1760 for task in self.scenequeue_notcovered:
1761 self.rq.scenequeue_notcovered.add(self.rqdata.runq_setscene[task])
1762
1763 logger.debug(1, 'We can skip tasks %s', sorted(self.rq.scenequeue_covered))
1764
1765 self.rq.state = runQueueRunInit
1766 return True
1767
1768 def runqueue_process_waitpid(self, task, status):
1769 task = self.rq.rqdata.runq_setscene.index(task)
1770
1771 RunQueueExecute.runqueue_process_waitpid(self, task, status)
1772
1773class TaskFailure(Exception):
1774 """
1775 Exception raised when a task in a runqueue fails
1776 """
1777 def __init__(self, x):
1778 self.args = x
1779
1780
1781class runQueueExitWait(bb.event.Event):
1782 """
1783 Event when waiting for task processes to exit
1784 """
1785
1786 def __init__(self, remain):
1787 self.remain = remain
1788 self.message = "Waiting for %s active tasks to finish" % remain
1789 bb.event.Event.__init__(self)
1790
1791class runQueueEvent(bb.event.Event):
1792 """
1793 Base runQueue event class
1794 """
1795 def __init__(self, task, stats, rq):
1796 self.taskid = task
1797 self.taskstring = rq.rqdata.get_user_idstring(task)
1798 self.taskname = rq.rqdata.get_task_name(task)
1799 self.taskfile = rq.rqdata.get_task_file(task)
1800 self.taskhash = rq.rqdata.get_task_hash(task)
1801 self.stats = stats.copy()
1802 bb.event.Event.__init__(self)
1803
1804class sceneQueueEvent(runQueueEvent):
1805 """
1806 Base sceneQueue event class
1807 """
1808 def __init__(self, task, stats, rq, noexec=False):
1809 runQueueEvent.__init__(self, task, stats, rq)
1810 realtask = rq.rqdata.runq_setscene[task]
1811 self.taskstring = rq.rqdata.get_user_idstring(realtask, "_setscene")
1812 self.taskname = rq.rqdata.get_task_name(realtask) + "_setscene"
1813 self.taskfile = rq.rqdata.get_task_file(realtask)
1814 self.taskhash = rq.rqdata.get_task_hash(task)
1815
1816class runQueueTaskStarted(runQueueEvent):
1817 """
1818 Event notifing a task was started
1819 """
1820 def __init__(self, task, stats, rq, noexec=False):
1821 runQueueEvent.__init__(self, task, stats, rq)
1822 self.noexec = noexec
1823
1824class sceneQueueTaskStarted(sceneQueueEvent):
1825 """
1826 Event notifing a setscene task was started
1827 """
1828 def __init__(self, task, stats, rq, noexec=False):
1829 sceneQueueEvent.__init__(self, task, stats, rq)
1830 self.noexec = noexec
1831
1832class runQueueTaskFailed(runQueueEvent):
1833 """
1834 Event notifing a task failed
1835 """
1836 def __init__(self, task, stats, exitcode, rq):
1837 runQueueEvent.__init__(self, task, stats, rq)
1838 self.exitcode = exitcode
1839
1840class sceneQueueTaskFailed(sceneQueueEvent):
1841 """
1842 Event notifing a setscene task failed
1843 """
1844 def __init__(self, task, stats, exitcode, rq):
1845 sceneQueueEvent.__init__(self, task, stats, rq)
1846 self.exitcode = exitcode
1847
1848class runQueueTaskCompleted(runQueueEvent):
1849 """
1850 Event notifing a task completed
1851 """
1852
1853class sceneQueueTaskCompleted(sceneQueueEvent):
1854 """
1855 Event notifing a setscene task completed
1856 """
1857
1858class runQueueTaskSkipped(runQueueEvent):
1859 """
1860 Event notifing a task was skipped
1861 """
1862 def __init__(self, task, stats, rq, reason):
1863 runQueueEvent.__init__(self, task, stats, rq)
1864 self.reason = reason
1865
1866class runQueuePipe():
1867 """
1868 Abstraction for a pipe between a worker thread and the server
1869 """
1870 def __init__(self, pipein, pipeout, d, rq):
1871 self.input = pipein
1872 if pipeout:
1873 pipeout.close()
1874 bb.utils.nonblockingfd(self.input)
1875 self.queue = ""
1876 self.d = d
1877 self.rq = rq
1878
1879 def setrunqueueexec(self, rq):
1880 self.rq = rq
1881
1882 def read(self):
1883 start = len(self.queue)
1884 try:
1885 self.queue = self.queue + self.input.read(102400)
1886 except (OSError, IOError) as e:
1887 if e.errno != errno.EAGAIN:
1888 raise
1889 end = len(self.queue)
1890 found = True
1891 while found and len(self.queue):
1892 found = False
1893 index = self.queue.find("</event>")
1894 while index != -1 and self.queue.startswith("<event>"):
1895 event = pickle.loads(self.queue[7:index])
1896 bb.event.fire_from_worker(event, self.d)
1897 found = True
1898 self.queue = self.queue[index+8:]
1899 index = self.queue.find("</event>")
1900 index = self.queue.find("</exitcode>")
1901 while index != -1 and self.queue.startswith("<exitcode>"):
1902 task, status = pickle.loads(self.queue[10:index])
1903 self.rq.runqueue_process_waitpid(task, status)
1904 found = True
1905 self.queue = self.queue[index+11:]
1906 index = self.queue.find("</exitcode>")
1907 return (end > start)
1908
1909 def close(self):
1910 while self.read():
1911 continue
1912 if len(self.queue) > 0:
1913 print("Warning, worker left partial message: %s" % self.queue)
1914 self.input.close()
diff --git a/bitbake/lib/bb/server/__init__.py b/bitbake/lib/bb/server/__init__.py
new file mode 100644
index 0000000000..da5e480740
--- /dev/null
+++ b/bitbake/lib/bb/server/__init__.py
@@ -0,0 +1,96 @@
1#
2# BitBake Base Server Code
3#
4# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer
5# Copyright (C) 2006 - 2008 Richard Purdie
6# Copyright (C) 2013 Alexandru Damian
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21""" Base code for Bitbake server process
22
23Have a common base for that all Bitbake server classes ensures a consistent
24approach to the interface, and minimize risks associated with code duplication.
25
26"""
27
28""" BaseImplServer() the base class for all XXServer() implementations.
29
30 These classes contain the actual code that runs the server side, i.e.
31 listens for the commands and executes them. Although these implementations
32 contain all the data of the original bitbake command, i.e the cooker instance,
33 they may well run on a different process or even machine.
34
35"""
36
37class BaseImplServer():
38 def __init__(self):
39 self._idlefuns = {}
40
41 def addcooker(self, cooker):
42 self.cooker = cooker
43
44 def register_idle_function(self, function, data):
45 """Register a function to be called while the server is idle"""
46 assert hasattr(function, '__call__')
47 self._idlefuns[function] = data
48
49
50
51""" BitBakeBaseServerConnection class is the common ancestor to all
52 BitBakeServerConnection classes.
53
54 These classes control the remote server. The only command currently
55 implemented is the terminate() command.
56
57"""
58
59class BitBakeBaseServerConnection():
60 def __init__(self, serverImpl):
61 pass
62
63 def terminate(self):
64 pass
65
66
67""" BitBakeBaseServer class is the common ancestor to all Bitbake servers
68
69 Derive this class in order to implement a BitBakeServer which is the
70 controlling stub for the actual server implementation
71
72"""
73class BitBakeBaseServer(object):
74 def initServer(self):
75 self.serverImpl = None # we ensure a runtime crash if not overloaded
76 self.connection = None
77 return
78
79 def addcooker(self, cooker):
80 self.cooker = cooker
81 self.serverImpl.addcooker(cooker)
82
83 def getServerIdleCB(self):
84 return self.serverImpl.register_idle_function
85
86 def saveConnectionDetails(self):
87 return
88
89 def detach(self):
90 return
91
92 def establishConnection(self, featureset):
93 raise "Must redefine the %s.establishConnection()" % self.__class__.__name__
94
95 def endSession(self):
96 self.connection.terminate()
diff --git a/bitbake/lib/bb/server/process.py b/bitbake/lib/bb/server/process.py
new file mode 100644
index 0000000000..aa072020af
--- /dev/null
+++ b/bitbake/lib/bb/server/process.py
@@ -0,0 +1,223 @@
1#
2# BitBake Process based server.
3#
4# Copyright (C) 2010 Bob Foerster <robert@erafx.com>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19"""
20 This module implements a multiprocessing.Process based server for bitbake.
21"""
22
23import bb
24import bb.event
25import itertools
26import logging
27import multiprocessing
28import os
29import signal
30import sys
31import time
32import select
33from Queue import Empty
34from multiprocessing import Event, Process, util, Queue, Pipe, queues, Manager
35
36from . import BitBakeBaseServer, BitBakeBaseServerConnection, BaseImplServer
37
38logger = logging.getLogger('BitBake')
39
40class ServerCommunicator():
41 def __init__(self, connection, event_handle):
42 self.connection = connection
43 self.event_handle = event_handle
44
45 def runCommand(self, command):
46 # @todo try/except
47 self.connection.send(command)
48
49 while True:
50 # don't let the user ctrl-c while we're waiting for a response
51 try:
52 if self.connection.poll(20):
53 return self.connection.recv()
54 else:
55 bb.fatal("Timeout while attempting to communicate with bitbake server")
56 except KeyboardInterrupt:
57 pass
58
59 def getEventHandle(self):
60 return self.event_handle.value
61
62class EventAdapter():
63 """
64 Adapter to wrap our event queue since the caller (bb.event) expects to
65 call a send() method, but our actual queue only has put()
66 """
67 def __init__(self, queue):
68 self.queue = queue
69
70 def send(self, event):
71 try:
72 self.queue.put(event)
73 except Exception as err:
74 print("EventAdapter puked: %s" % str(err))
75
76
77class ProcessServer(Process, BaseImplServer):
78 profile_filename = "profile.log"
79 profile_processed_filename = "profile.log.processed"
80
81 def __init__(self, command_channel, event_queue, featurelist):
82 BaseImplServer.__init__(self)
83 Process.__init__(self, args=(featurelist))
84 self.command_channel = command_channel
85 self.event_queue = event_queue
86 self.event = EventAdapter(event_queue)
87 self.featurelist = featurelist
88 self.quit = False
89
90 self.keep_running = Event()
91 self.keep_running.set()
92 self.event_handle = multiprocessing.Value("i")
93
94 def run(self):
95 for event in bb.event.ui_queue:
96 self.event_queue.put(event)
97 self.event_handle.value = bb.event.register_UIHhandler(self)
98
99 # process any feature changes based on what UI requested
100 original_featureset = list(self.cooker.featureset)
101 while len(self.featurelist)> 0:
102 self.cooker.featureset.setFeature(self.featurelist.pop())
103 if (original_featureset != list(self.cooker.featureset)):
104 self.cooker.reset()
105
106 bb.cooker.server_main(self.cooker, self.main)
107
108 def main(self):
109 # Ignore SIGINT within the server, as all SIGINT handling is done by
110 # the UI and communicated to us
111 signal.signal(signal.SIGINT, signal.SIG_IGN)
112 while self.keep_running.is_set():
113 try:
114 if self.command_channel.poll():
115 command = self.command_channel.recv()
116 self.runCommand(command)
117
118 self.idle_commands(.1, [self.event_queue._reader, self.command_channel])
119 except Exception:
120 logger.exception('Running command %s', command)
121
122 self.event_queue.close()
123 bb.event.unregister_UIHhandler(self.event_handle.value)
124 self.command_channel.close()
125 self.cooker.shutdown(True)
126 self.idle_commands(.1)
127
128 def idle_commands(self, delay, fds = []):
129 nextsleep = delay
130
131 for function, data in self._idlefuns.items():
132 try:
133 retval = function(self, data, False)
134 if retval is False:
135 del self._idlefuns[function]
136 elif retval is True:
137 nextsleep = None
138 elif nextsleep is None:
139 continue
140 else:
141 fds = fds + retval
142 except SystemExit:
143 raise
144 except Exception:
145 logger.exception('Running idle function')
146
147 if nextsleep is not None:
148 select.select(fds,[],[],nextsleep)
149
150 def runCommand(self, command):
151 """
152 Run a cooker command on the server
153 """
154 self.command_channel.send(self.cooker.command.runCommand(command))
155
156 def stop(self):
157 self.keep_running.clear()
158
159class BitBakeProcessServerConnection(BitBakeBaseServerConnection):
160 def __init__(self, serverImpl, ui_channel, event_queue):
161 self.procserver = serverImpl
162 self.ui_channel = ui_channel
163 self.event_queue = event_queue
164 self.connection = ServerCommunicator(self.ui_channel, self.procserver.event_handle)
165 self.events = self.event_queue
166
167 def terminate(self):
168 def flushevents():
169 while True:
170 try:
171 event = self.event_queue.get(block=False)
172 except (Empty, IOError):
173 break
174 if isinstance(event, logging.LogRecord):
175 logger.handle(event)
176
177 signal.signal(signal.SIGINT, signal.SIG_IGN)
178 self.procserver.stop()
179
180 while self.procserver.is_alive():
181 flushevents()
182 self.procserver.join(0.1)
183
184 self.ui_channel.close()
185 self.event_queue.close()
186
187# Wrap Queue to provide API which isn't server implementation specific
188class ProcessEventQueue(multiprocessing.queues.Queue):
189 def waitEvent(self, timeout):
190 try:
191 return self.get(True, timeout)
192 except Empty:
193 return None
194
195 def getEvent(self):
196 try:
197 return self.get(False)
198 except Empty:
199 return None
200
201
202class BitBakeServer(BitBakeBaseServer):
203 def initServer(self):
204 # establish communication channels. We use bidirectional pipes for
205 # ui <--> server command/response pairs
206 # and a queue for server -> ui event notifications
207 #
208 self.ui_channel, self.server_channel = Pipe()
209 self.event_queue = ProcessEventQueue(0)
210 manager = Manager()
211 self.featurelist = manager.list()
212 self.serverImpl = ProcessServer(self.server_channel, self.event_queue, self.featurelist)
213
214 def detach(self):
215 self.serverImpl.start()
216 return
217
218 def establishConnection(self, featureset):
219 for f in featureset:
220 self.featurelist.append(f)
221 self.connection = BitBakeProcessServerConnection(self.serverImpl, self.ui_channel, self.event_queue)
222 signal.signal(signal.SIGTERM, lambda i, s: self.connection.terminate())
223 return self.connection
diff --git a/bitbake/lib/bb/server/xmlrpc.py b/bitbake/lib/bb/server/xmlrpc.py
new file mode 100644
index 0000000000..82c0e8d8a6
--- /dev/null
+++ b/bitbake/lib/bb/server/xmlrpc.py
@@ -0,0 +1,365 @@
1#
2# BitBake XMLRPC Server
3#
4# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer
5# Copyright (C) 2006 - 2008 Richard Purdie
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20"""
21 This module implements an xmlrpc server for BitBake.
22
23 Use this by deriving a class from BitBakeXMLRPCServer and then adding
24 methods which you want to "export" via XMLRPC. If the methods have the
25 prefix xmlrpc_, then registering those function will happen automatically,
26 if not, you need to call register_function.
27
28 Use register_idle_function() to add a function which the xmlrpc server
29 calls from within server_forever when no requests are pending. Make sure
30 that those functions are non-blocking or else you will introduce latency
31 in the server's main loop.
32"""
33
34import bb
35import xmlrpclib, sys
36from bb import daemonize
37from bb.ui import uievent
38import hashlib, time
39import socket
40import os, signal
41import threading
42try:
43 import cPickle as pickle
44except ImportError:
45 import pickle
46
47DEBUG = False
48
49from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
50import inspect, select, httplib
51
52from . import BitBakeBaseServer, BitBakeBaseServerConnection, BaseImplServer
53
54class BBTransport(xmlrpclib.Transport):
55 def __init__(self, timeout):
56 self.timeout = timeout
57 self.connection_token = None
58 xmlrpclib.Transport.__init__(self)
59
60 # Modified from default to pass timeout to HTTPConnection
61 def make_connection(self, host):
62 #return an existing connection if possible. This allows
63 #HTTP/1.1 keep-alive.
64 if self._connection and host == self._connection[0]:
65 return self._connection[1]
66
67 # create a HTTP connection object from a host descriptor
68 chost, self._extra_headers, x509 = self.get_host_info(host)
69 #store the host argument along with the connection object
70 self._connection = host, httplib.HTTPConnection(chost, timeout=self.timeout)
71 return self._connection[1]
72
73 def set_connection_token(self, token):
74 self.connection_token = token
75
76 def send_content(self, h, body):
77 if self.connection_token:
78 h.putheader("Bitbake-token", self.connection_token)
79 xmlrpclib.Transport.send_content(self, h, body)
80
81def _create_server(host, port, timeout = 60):
82 t = BBTransport(timeout)
83 s = xmlrpclib.Server("http://%s:%d/" % (host, port), transport=t, allow_none=True)
84 return s, t
85
86class BitBakeServerCommands():
87
88 def __init__(self, server):
89 self.server = server
90 self.has_client = False
91
92 def registerEventHandler(self, host, port, featureset = []):
93 """
94 Register a remote UI Event Handler
95 """
96 s, t = _create_server(host, port)
97
98 # we don't allow connections if the cooker is running
99 if (self.cooker.state in [bb.cooker.state.parsing, bb.cooker.state.running]):
100 return None
101
102 original_featureset = list(self.cooker.featureset)
103 for f in featureset:
104 self.cooker.featureset.setFeature(f)
105
106 if (original_featureset != list(self.cooker.featureset)):
107 self.cooker.reset()
108
109 self.event_handle = bb.event.register_UIHhandler(s)
110 return self.event_handle
111
112 def unregisterEventHandler(self, handlerNum):
113 """
114 Unregister a remote UI Event Handler
115 """
116 return bb.event.unregister_UIHhandler(handlerNum)
117
118 def runCommand(self, command):
119 """
120 Run a cooker command on the server
121 """
122 return self.cooker.command.runCommand(command, self.server.readonly)
123
124 def getEventHandle(self):
125 return self.event_handle
126
127 def terminateServer(self):
128 """
129 Trigger the server to quit
130 """
131 self.server.quit = True
132 print("Server (cooker) exiting")
133 return
134
135 def addClient(self):
136 if self.has_client:
137 return None
138 token = hashlib.md5(str(time.time())).hexdigest()
139 self.server.set_connection_token(token)
140 self.has_client = True
141 return token
142
143 def removeClient(self):
144 if self.has_client:
145 self.server.set_connection_token(None)
146 self.has_client = False
147 if self.server.single_use:
148 self.server.quit = True
149
150# This request handler checks if the request has a "Bitbake-token" header
151# field (this comes from the client side) and compares it with its internal
152# "Bitbake-token" field (this comes from the server). If the two are not
153# equal, it is assumed that a client is trying to connect to the server
154# while another client is connected to the server. In this case, a 503 error
155# ("service unavailable") is returned to the client.
156class BitBakeXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
157 def __init__(self, request, client_address, server):
158 self.server = server
159 SimpleXMLRPCRequestHandler.__init__(self, request, client_address, server)
160
161 def do_POST(self):
162 try:
163 remote_token = self.headers["Bitbake-token"]
164 except:
165 remote_token = None
166 if remote_token != self.server.connection_token and remote_token != "observer":
167 self.report_503()
168 else:
169 if remote_token == "observer":
170 self.server.readonly = True
171 else:
172 self.server.readonly = False
173 SimpleXMLRPCRequestHandler.do_POST(self)
174
175 def report_503(self):
176 self.send_response(503)
177 response = 'No more client allowed'
178 self.send_header("Content-type", "text/plain")
179 self.send_header("Content-length", str(len(response)))
180 self.end_headers()
181 self.wfile.write(response)
182
183
184class XMLRPCProxyServer(BaseImplServer):
185 """ not a real working server, but a stub for a proxy server connection
186
187 """
188 def __init__(self, host, port):
189 self.host = host
190 self.port = port
191
192class XMLRPCServer(SimpleXMLRPCServer, BaseImplServer):
193 # remove this when you're done with debugging
194 # allow_reuse_address = True
195
196 def __init__(self, interface):
197 """
198 Constructor
199 """
200 BaseImplServer.__init__(self)
201 SimpleXMLRPCServer.__init__(self, interface,
202 requestHandler=BitBakeXMLRPCRequestHandler,
203 logRequests=False, allow_none=True)
204 self.host, self.port = self.socket.getsockname()
205 self.connection_token = None
206 #self.register_introspection_functions()
207 self.commands = BitBakeServerCommands(self)
208 self.autoregister_all_functions(self.commands, "")
209 self.interface = interface
210 self.single_use = False
211 if (interface[1] == 0): # anonymous port, not getting reused
212 self.single_use = True
213
214 def addcooker(self, cooker):
215 BaseImplServer.addcooker(self, cooker)
216 self.commands.cooker = cooker
217
218 def autoregister_all_functions(self, context, prefix):
219 """
220 Convenience method for registering all functions in the scope
221 of this class that start with a common prefix
222 """
223 methodlist = inspect.getmembers(context, inspect.ismethod)
224 for name, method in methodlist:
225 if name.startswith(prefix):
226 self.register_function(method, name[len(prefix):])
227
228
229 def serve_forever(self):
230 # Start the actual XMLRPC server
231 bb.cooker.server_main(self.cooker, self._serve_forever)
232
233 def _serve_forever(self):
234 """
235 Serve Requests. Overloaded to honor a quit command
236 """
237 self.quit = False
238 while not self.quit:
239 fds = [self]
240 nextsleep = 0.1
241 for function, data in self._idlefuns.items():
242 try:
243 retval = function(self, data, False)
244 if retval is False:
245 del self._idlefuns[function]
246 elif retval is True:
247 nextsleep = 0
248 else:
249 fds = fds + retval
250 except SystemExit:
251 raise
252 except:
253 import traceback
254 traceback.print_exc()
255 pass
256
257 socktimeout = self.socket.gettimeout() or nextsleep
258 socktimeout = min(socktimeout, nextsleep)
259 # Mirror what BaseServer handle_request would do
260 fd_sets = select.select(fds, [], [], socktimeout)
261 if fd_sets[0] and self in fd_sets[0]:
262 self._handle_request_noblock()
263
264 # Tell idle functions we're exiting
265 for function, data in self._idlefuns.items():
266 try:
267 retval = function(self, data, True)
268 except:
269 pass
270 self.server_close()
271 return
272
273 def set_connection_token(self, token):
274 self.connection_token = token
275
276class BitBakeXMLRPCServerConnection(BitBakeBaseServerConnection):
277 def __init__(self, serverImpl, clientinfo=("localhost", 0), observer_only = False, featureset = []):
278 self.connection, self.transport = _create_server(serverImpl.host, serverImpl.port)
279 self.clientinfo = clientinfo
280 self.serverImpl = serverImpl
281 self.observer_only = observer_only
282 self.featureset = featureset
283
284 def connect(self):
285 if not self.observer_only:
286 token = self.connection.addClient()
287 else:
288 token = "observer"
289 if token is None:
290 return None
291 self.transport.set_connection_token(token)
292
293 self.events = uievent.BBUIEventQueue(self.connection, self.clientinfo, self.featureset)
294 for event in bb.event.ui_queue:
295 self.events.queue_event(event)
296 return self
297
298 def removeClient(self):
299 if not self.observer_only:
300 self.connection.removeClient()
301
302 def terminate(self):
303 # Don't wait for server indefinitely
304 import socket
305 socket.setdefaulttimeout(2)
306 try:
307 self.events.system_quit()
308 except:
309 pass
310 try:
311 self.connection.removeClient()
312 except:
313 pass
314
315class BitBakeServer(BitBakeBaseServer):
316 def initServer(self, interface = ("localhost", 0)):
317 self.interface = interface
318 self.serverImpl = XMLRPCServer(interface)
319
320 def detach(self):
321 daemonize.createDaemon(self.serverImpl.serve_forever, "bitbake-cookerdaemon.log")
322 del self.cooker
323
324 def establishConnection(self, featureset):
325 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, self.interface, False, featureset)
326 return self.connection.connect()
327
328 def set_connection_token(self, token):
329 self.connection.transport.set_connection_token(token)
330
331class BitBakeXMLRPCClient(BitBakeBaseServer):
332
333 def __init__(self, observer_only = False):
334 self.observer_only = observer_only
335 # if we need extra caches, just tell the server to load them all
336 pass
337
338 def saveConnectionDetails(self, remote):
339 self.remote = remote
340
341 def establishConnection(self, featureset):
342 # The format of "remote" must be "server:port"
343 try:
344 [host, port] = self.remote.split(":")
345 port = int(port)
346 except:
347 return None
348 # We need our IP for the server connection. We get the IP
349 # by trying to connect with the server
350 try:
351 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
352 s.connect((host, port))
353 ip = s.getsockname()[0]
354 s.close()
355 except:
356 return None
357 try:
358 self.serverImpl = XMLRPCProxyServer(host, port)
359 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, (ip, 0), self.observer_only, featureset)
360 return self.connection.connect()
361 except Exception as e:
362 bb.fatal("Could not connect to server at %s:%s (%s)" % (host, port, str(e)))
363
364 def endSession(self):
365 self.connection.removeClient()
diff --git a/bitbake/lib/bb/shell.py b/bitbake/lib/bb/shell.py
new file mode 100644
index 0000000000..1dd8d54bdb
--- /dev/null
+++ b/bitbake/lib/bb/shell.py
@@ -0,0 +1,820 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3##########################################################################
4#
5# Copyright (C) 2005-2006 Michael 'Mickey' Lauer <mickey@Vanille.de>
6# Copyright (C) 2005-2006 Vanille Media
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21##########################################################################
22#
23# Thanks to:
24# * Holger Freyther <zecke@handhelds.org>
25# * Justin Patrin <papercrane@reversefold.com>
26#
27##########################################################################
28
29"""
30BitBake Shell
31
32IDEAS:
33 * list defined tasks per package
34 * list classes
35 * toggle force
36 * command to reparse just one (or more) bbfile(s)
37 * automatic check if reparsing is necessary (inotify?)
38 * frontend for bb file manipulation
39 * more shell-like features:
40 - output control, i.e. pipe output into grep, sort, etc.
41 - job control, i.e. bring running commands into background and foreground
42 * start parsing in background right after startup
43 * ncurses interface
44
45PROBLEMS:
46 * force doesn't always work
47 * readline completion for commands with more than one parameters
48
49"""
50
51##########################################################################
52# Import and setup global variables
53##########################################################################
54
55from __future__ import print_function
56from functools import reduce
57try:
58 set
59except NameError:
60 from sets import Set as set
61import sys, os, readline, socket, httplib, urllib, commands, popen2, shlex, Queue, fnmatch
62from bb import data, parse, build, cache, taskdata, runqueue, providers as Providers
63
64__version__ = "0.5.3.1"
65__credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
66Type 'help' for more information, press CTRL-D to exit.""" % __version__
67
68cmds = {}
69leave_mainloop = False
70last_exception = None
71cooker = None
72parsed = False
73debug = os.environ.get( "BBSHELL_DEBUG", "" )
74
75##########################################################################
76# Class BitBakeShellCommands
77##########################################################################
78
79class BitBakeShellCommands:
80 """This class contains the valid commands for the shell"""
81
82 def __init__( self, shell ):
83 """Register all the commands"""
84 self._shell = shell
85 for attr in BitBakeShellCommands.__dict__:
86 if not attr.startswith( "_" ):
87 if attr.endswith( "_" ):
88 command = attr[:-1].lower()
89 else:
90 command = attr[:].lower()
91 method = getattr( BitBakeShellCommands, attr )
92 debugOut( "registering command '%s'" % command )
93 # scan number of arguments
94 usage = getattr( method, "usage", "" )
95 if usage != "<...>":
96 numArgs = len( usage.split() )
97 else:
98 numArgs = -1
99 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
100
101 def _checkParsed( self ):
102 if not parsed:
103 print("SHELL: This command needs to parse bbfiles...")
104 self.parse( None )
105
106 def _findProvider( self, item ):
107 self._checkParsed()
108 # Need to use taskData for this information
109 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
110 if not preferred: preferred = item
111 try:
112 lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status)
113 except KeyError:
114 if item in cooker.status.providers:
115 pf = cooker.status.providers[item][0]
116 else:
117 pf = None
118 return pf
119
120 def alias( self, params ):
121 """Register a new name for a command"""
122 new, old = params
123 if not old in cmds:
124 print("ERROR: Command '%s' not known" % old)
125 else:
126 cmds[new] = cmds[old]
127 print("OK")
128 alias.usage = "<alias> <command>"
129
130 def buffer( self, params ):
131 """Dump specified output buffer"""
132 index = params[0]
133 print(self._shell.myout.buffer( int( index ) ))
134 buffer.usage = "<index>"
135
136 def buffers( self, params ):
137 """Show the available output buffers"""
138 commands = self._shell.myout.bufferedCommands()
139 if not commands:
140 print("SHELL: No buffered commands available yet. Start doing something.")
141 else:
142 print("="*35, "Available Output Buffers", "="*27)
143 for index, cmd in enumerate( commands ):
144 print("| %s %s" % ( str( index ).ljust( 3 ), cmd ))
145 print("="*88)
146
147 def build( self, params, cmd = "build" ):
148 """Build a providee"""
149 global last_exception
150 globexpr = params[0]
151 self._checkParsed()
152 names = globfilter( cooker.status.pkg_pn, globexpr )
153 if len( names ) == 0: names = [ globexpr ]
154 print("SHELL: Building %s" % ' '.join( names ))
155
156 td = taskdata.TaskData(cooker.configuration.abort)
157 localdata = data.createCopy(cooker.configuration.data)
158 data.update_data(localdata)
159 data.expandKeys(localdata)
160
161 try:
162 tasks = []
163 for name in names:
164 td.add_provider(localdata, cooker.status, name)
165 providers = td.get_provider(name)
166
167 if len(providers) == 0:
168 raise Providers.NoProvider
169
170 tasks.append([name, "do_%s" % cmd])
171
172 td.add_unresolved(localdata, cooker.status)
173
174 rq = runqueue.RunQueue(cooker, localdata, cooker.status, td, tasks)
175 rq.prepare_runqueue()
176 rq.execute_runqueue()
177
178 except Providers.NoProvider:
179 print("ERROR: No Provider")
180 last_exception = Providers.NoProvider
181
182 except runqueue.TaskFailure as fnids:
183 last_exception = runqueue.TaskFailure
184
185 except build.FuncFailed as e:
186 print("ERROR: Couldn't build '%s'" % names)
187 last_exception = e
188
189
190 build.usage = "<providee>"
191
192 def clean( self, params ):
193 """Clean a providee"""
194 self.build( params, "clean" )
195 clean.usage = "<providee>"
196
197 def compile( self, params ):
198 """Execute 'compile' on a providee"""
199 self.build( params, "compile" )
200 compile.usage = "<providee>"
201
202 def configure( self, params ):
203 """Execute 'configure' on a providee"""
204 self.build( params, "configure" )
205 configure.usage = "<providee>"
206
207 def install( self, params ):
208 """Execute 'install' on a providee"""
209 self.build( params, "install" )
210 install.usage = "<providee>"
211
212 def edit( self, params ):
213 """Call $EDITOR on a providee"""
214 name = params[0]
215 bbfile = self._findProvider( name )
216 if bbfile is not None:
217 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
218 else:
219 print("ERROR: Nothing provides '%s'" % name)
220 edit.usage = "<providee>"
221
222 def environment( self, params ):
223 """Dump out the outer BitBake environment"""
224 cooker.showEnvironment()
225
226 def exit_( self, params ):
227 """Leave the BitBake Shell"""
228 debugOut( "setting leave_mainloop to true" )
229 global leave_mainloop
230 leave_mainloop = True
231
232 def fetch( self, params ):
233 """Fetch a providee"""
234 self.build( params, "fetch" )
235 fetch.usage = "<providee>"
236
237 def fileBuild( self, params, cmd = "build" ):
238 """Parse and build a .bb file"""
239 global last_exception
240 name = params[0]
241 bf = completeFilePath( name )
242 print("SHELL: Calling '%s' on '%s'" % ( cmd, bf ))
243
244 try:
245 cooker.buildFile(bf, cmd)
246 except parse.ParseError:
247 print("ERROR: Unable to open or parse '%s'" % bf)
248 except build.FuncFailed as e:
249 print("ERROR: Couldn't build '%s'" % name)
250 last_exception = e
251
252 fileBuild.usage = "<bbfile>"
253
254 def fileClean( self, params ):
255 """Clean a .bb file"""
256 self.fileBuild( params, "clean" )
257 fileClean.usage = "<bbfile>"
258
259 def fileEdit( self, params ):
260 """Call $EDITOR on a .bb file"""
261 name = params[0]
262 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
263 fileEdit.usage = "<bbfile>"
264
265 def fileRebuild( self, params ):
266 """Rebuild (clean & build) a .bb file"""
267 self.fileBuild( params, "rebuild" )
268 fileRebuild.usage = "<bbfile>"
269
270 def fileReparse( self, params ):
271 """(re)Parse a bb file"""
272 bbfile = params[0]
273 print("SHELL: Parsing '%s'" % bbfile)
274 parse.update_mtime( bbfile )
275 cooker.parser.reparse(bbfile)
276 if False: #fromCache:
277 print("SHELL: File has not been updated, not reparsing")
278 else:
279 print("SHELL: Parsed")
280 fileReparse.usage = "<bbfile>"
281
282 def abort( self, params ):
283 """Toggle abort task execution flag (see bitbake -k)"""
284 cooker.configuration.abort = not cooker.configuration.abort
285 print("SHELL: Abort Flag is now '%s'" % repr( cooker.configuration.abort ))
286
287 def force( self, params ):
288 """Toggle force task execution flag (see bitbake -f)"""
289 cooker.configuration.force = not cooker.configuration.force
290 print("SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force ))
291
292 def help( self, params ):
293 """Show a comprehensive list of commands and their purpose"""
294 print("="*30, "Available Commands", "="*30)
295 for cmd in sorted(cmds):
296 function, numparams, usage, helptext = cmds[cmd]
297 print("| %s | %s" % (usage.ljust(30), helptext))
298 print("="*78)
299
300 def lastError( self, params ):
301 """Show the reason or log that was produced by the last BitBake event exception"""
302 if last_exception is None:
303 print("SHELL: No Errors yet (Phew)...")
304 else:
305 reason, event = last_exception.args
306 print("SHELL: Reason for the last error: '%s'" % reason)
307 if ':' in reason:
308 msg, filename = reason.split( ':' )
309 filename = filename.strip()
310 print("SHELL: Dumping log file for last error:")
311 try:
312 print(open( filename ).read())
313 except IOError:
314 print("ERROR: Couldn't open '%s'" % filename)
315
316 def match( self, params ):
317 """Dump all files or providers matching a glob expression"""
318 what, globexpr = params
319 if what == "files":
320 self._checkParsed()
321 for key in globfilter( cooker.status.pkg_fn, globexpr ): print(key)
322 elif what == "providers":
323 self._checkParsed()
324 for key in globfilter( cooker.status.pkg_pn, globexpr ): print(key)
325 else:
326 print("Usage: match %s" % self.print_.usage)
327 match.usage = "<files|providers> <glob>"
328
329 def new( self, params ):
330 """Create a new .bb file and open the editor"""
331 dirname, filename = params
332 packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
333 fulldirname = "%s/%s" % ( packages, dirname )
334
335 if not os.path.exists( fulldirname ):
336 print("SHELL: Creating '%s'" % fulldirname)
337 os.mkdir( fulldirname )
338 if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
339 if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
340 print("SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename ))
341 return False
342 print("SHELL: Creating '%s/%s'" % ( fulldirname, filename ))
343 newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
344 print("""DESCRIPTION = ""
345SECTION = ""
346AUTHOR = ""
347HOMEPAGE = ""
348MAINTAINER = ""
349LICENSE = "GPL"
350PR = "r0"
351
352SRC_URI = ""
353
354#inherit base
355
356#do_configure() {
357#
358#}
359
360#do_compile() {
361#
362#}
363
364#do_stage() {
365#
366#}
367
368#do_install() {
369#
370#}
371""", file=newpackage)
372 newpackage.close()
373 os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
374 new.usage = "<directory> <filename>"
375
376 def package( self, params ):
377 """Execute 'package' on a providee"""
378 self.build( params, "package" )
379 package.usage = "<providee>"
380
381 def pasteBin( self, params ):
382 """Send a command + output buffer to the pastebin at http://rafb.net/paste"""
383 index = params[0]
384 contents = self._shell.myout.buffer( int( index ) )
385 sendToPastebin( "output of " + params[0], contents )
386 pasteBin.usage = "<index>"
387
388 def pasteLog( self, params ):
389 """Send the last event exception error log (if there is one) to http://rafb.net/paste"""
390 if last_exception is None:
391 print("SHELL: No Errors yet (Phew)...")
392 else:
393 reason, event = last_exception.args
394 print("SHELL: Reason for the last error: '%s'" % reason)
395 if ':' in reason:
396 msg, filename = reason.split( ':' )
397 filename = filename.strip()
398 print("SHELL: Pasting log file to pastebin...")
399
400 file = open( filename ).read()
401 sendToPastebin( "contents of " + filename, file )
402
403 def patch( self, params ):
404 """Execute 'patch' command on a providee"""
405 self.build( params, "patch" )
406 patch.usage = "<providee>"
407
408 def parse( self, params ):
409 """(Re-)parse .bb files and calculate the dependency graph"""
410 cooker.status = cache.CacheData(cooker.caches_array)
411 ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
412 cooker.status.ignored_dependencies = set( ignore.split() )
413 cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
414
415 (filelist, masked) = cooker.collect_bbfiles()
416 cooker.parse_bbfiles(filelist, masked, cooker.myProgressCallback)
417 cooker.buildDepgraph()
418 global parsed
419 parsed = True
420 print()
421
422 def reparse( self, params ):
423 """(re)Parse a providee's bb file"""
424 bbfile = self._findProvider( params[0] )
425 if bbfile is not None:
426 print("SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] ))
427 self.fileReparse( [ bbfile ] )
428 else:
429 print("ERROR: Nothing provides '%s'" % params[0])
430 reparse.usage = "<providee>"
431
432 def getvar( self, params ):
433 """Dump the contents of an outer BitBake environment variable"""
434 var = params[0]
435 value = data.getVar( var, cooker.configuration.data, 1 )
436 print(value)
437 getvar.usage = "<variable>"
438
439 def peek( self, params ):
440 """Dump contents of variable defined in providee's metadata"""
441 name, var = params
442 bbfile = self._findProvider( name )
443 if bbfile is not None:
444 the_data = cache.Cache.loadDataFull(bbfile, cooker.configuration.data)
445 value = the_data.getVar( var, 1 )
446 print(value)
447 else:
448 print("ERROR: Nothing provides '%s'" % name)
449 peek.usage = "<providee> <variable>"
450
451 def poke( self, params ):
452 """Set contents of variable defined in providee's metadata"""
453 name, var, value = params
454 bbfile = self._findProvider( name )
455 if bbfile is not None:
456 print("ERROR: Sorry, this functionality is currently broken")
457 #d = cooker.pkgdata[bbfile]
458 #data.setVar( var, value, d )
459
460 # mark the change semi persistant
461 #cooker.pkgdata.setDirty(bbfile, d)
462 #print "OK"
463 else:
464 print("ERROR: Nothing provides '%s'" % name)
465 poke.usage = "<providee> <variable> <value>"
466
467 def print_( self, params ):
468 """Dump all files or providers"""
469 what = params[0]
470 if what == "files":
471 self._checkParsed()
472 for key in cooker.status.pkg_fn: print(key)
473 elif what == "providers":
474 self._checkParsed()
475 for key in cooker.status.providers: print(key)
476 else:
477 print("Usage: print %s" % self.print_.usage)
478 print_.usage = "<files|providers>"
479
480 def python( self, params ):
481 """Enter the expert mode - an interactive BitBake Python Interpreter"""
482 sys.ps1 = "EXPERT BB>>> "
483 sys.ps2 = "EXPERT BB... "
484 import code
485 interpreter = code.InteractiveConsole( dict( globals() ) )
486 interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
487
488 def showdata( self, params ):
489 """Execute 'showdata' on a providee"""
490 cooker.showEnvironment(None, params)
491 showdata.usage = "<providee>"
492
493 def setVar( self, params ):
494 """Set an outer BitBake environment variable"""
495 var, value = params
496 data.setVar( var, value, cooker.configuration.data )
497 print("OK")
498 setVar.usage = "<variable> <value>"
499
500 def rebuild( self, params ):
501 """Clean and rebuild a .bb file or a providee"""
502 self.build( params, "clean" )
503 self.build( params, "build" )
504 rebuild.usage = "<providee>"
505
506 def shell( self, params ):
507 """Execute a shell command and dump the output"""
508 if params != "":
509 print(commands.getoutput( " ".join( params ) ))
510 shell.usage = "<...>"
511
512 def stage( self, params ):
513 """Execute 'stage' on a providee"""
514 self.build( params, "populate_staging" )
515 stage.usage = "<providee>"
516
517 def status( self, params ):
518 """<just for testing>"""
519 print("-" * 78)
520 print("building list = '%s'" % cooker.building_list)
521 print("build path = '%s'" % cooker.build_path)
522 print("consider_msgs_cache = '%s'" % cooker.consider_msgs_cache)
523 print("build stats = '%s'" % cooker.stats)
524 if last_exception is not None: print("last_exception = '%s'" % repr( last_exception.args ))
525 print("memory output contents = '%s'" % self._shell.myout._buffer)
526
527 def test( self, params ):
528 """<just for testing>"""
529 print("testCommand called with '%s'" % params)
530
531 def unpack( self, params ):
532 """Execute 'unpack' on a providee"""
533 self.build( params, "unpack" )
534 unpack.usage = "<providee>"
535
536 def which( self, params ):
537 """Computes the providers for a given providee"""
538 # Need to use taskData for this information
539 item = params[0]
540
541 self._checkParsed()
542
543 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
544 if not preferred: preferred = item
545
546 try:
547 lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status)
548 except KeyError:
549 lv, lf, pv, pf = (None,)*4
550
551 try:
552 providers = cooker.status.providers[item]
553 except KeyError:
554 print("SHELL: ERROR: Nothing provides", preferred)
555 else:
556 for provider in providers:
557 if provider == pf: provider = " (***) %s" % provider
558 else: provider = " %s" % provider
559 print(provider)
560 which.usage = "<providee>"
561
562##########################################################################
563# Common helper functions
564##########################################################################
565
566def completeFilePath( bbfile ):
567 """Get the complete bbfile path"""
568 if not cooker.status: return bbfile
569 if not cooker.status.pkg_fn: return bbfile
570 for key in cooker.status.pkg_fn:
571 if key.endswith( bbfile ):
572 return key
573 return bbfile
574
575def sendToPastebin( desc, content ):
576 """Send content to http://oe.pastebin.com"""
577 mydata = {}
578 mydata["lang"] = "Plain Text"
579 mydata["desc"] = desc
580 mydata["cvt_tabs"] = "No"
581 mydata["nick"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
582 mydata["text"] = content
583 params = urllib.urlencode( mydata )
584 headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
585
586 host = "rafb.net"
587 conn = httplib.HTTPConnection( "%s:80" % host )
588 conn.request("POST", "/paste/paste.php", params, headers )
589
590 response = conn.getresponse()
591 conn.close()
592
593 if response.status == 302:
594 location = response.getheader( "location" ) or "unknown"
595 print("SHELL: Pasted to http://%s%s" % ( host, location ))
596 else:
597 print("ERROR: %s %s" % ( response.status, response.reason ))
598
599def completer( text, state ):
600 """Return a possible readline completion"""
601 debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
602
603 if state == 0:
604 line = readline.get_line_buffer()
605 if " " in line:
606 line = line.split()
607 # we are in second (or more) argument
608 if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
609 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
610 if u == "<variable>":
611 allmatches = cooker.configuration.data.keys()
612 elif u == "<bbfile>":
613 if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
614 else: allmatches = [ x.split("/")[-1] for x in cooker.status.pkg_fn ]
615 elif u == "<providee>":
616 if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
617 else: allmatches = cooker.status.providers.iterkeys()
618 else: allmatches = [ "(No tab completion available for this command)" ]
619 else: allmatches = [ "(No tab completion available for this command)" ]
620 else:
621 # we are in first argument
622 allmatches = cmds.iterkeys()
623
624 completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
625 #print "completer.matches = '%s'" % completer.matches
626 if len( completer.matches ) > state:
627 return completer.matches[state]
628 else:
629 return None
630
631def debugOut( text ):
632 if debug:
633 sys.stderr.write( "( %s )\n" % text )
634
635def columnize( alist, width = 80 ):
636 """
637 A word-wrap function that preserves existing line breaks
638 and most spaces in the text. Expects that existing line
639 breaks are posix newlines (\n).
640 """
641 return reduce(lambda line, word, width=width: '%s%s%s' %
642 (line,
643 ' \n'[(len(line[line.rfind('\n')+1:])
644 + len(word.split('\n', 1)[0]
645 ) >= width)],
646 word),
647 alist
648 )
649
650def globfilter( names, pattern ):
651 return fnmatch.filter( names, pattern )
652
653##########################################################################
654# Class MemoryOutput
655##########################################################################
656
657class MemoryOutput:
658 """File-like output class buffering the output of the last 10 commands"""
659 def __init__( self, delegate ):
660 self.delegate = delegate
661 self._buffer = []
662 self.text = []
663 self._command = None
664
665 def startCommand( self, command ):
666 self._command = command
667 self.text = []
668 def endCommand( self ):
669 if self._command is not None:
670 if len( self._buffer ) == 10: del self._buffer[0]
671 self._buffer.append( ( self._command, self.text ) )
672 def removeLast( self ):
673 if self._buffer:
674 del self._buffer[ len( self._buffer ) - 1 ]
675 self.text = []
676 self._command = None
677 def lastBuffer( self ):
678 if self._buffer:
679 return self._buffer[ len( self._buffer ) -1 ][1]
680 def bufferedCommands( self ):
681 return [ cmd for cmd, output in self._buffer ]
682 def buffer( self, i ):
683 if i < len( self._buffer ):
684 return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
685 else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
686 def write( self, text ):
687 if self._command is not None and text != "BB>> ": self.text.append( text )
688 if self.delegate is not None: self.delegate.write( text )
689 def flush( self ):
690 return self.delegate.flush()
691 def fileno( self ):
692 return self.delegate.fileno()
693 def isatty( self ):
694 return self.delegate.isatty()
695
696##########################################################################
697# Class BitBakeShell
698##########################################################################
699
700class BitBakeShell:
701
702 def __init__( self ):
703 """Register commands and set up readline"""
704 self.commandQ = Queue.Queue()
705 self.commands = BitBakeShellCommands( self )
706 self.myout = MemoryOutput( sys.stdout )
707 self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
708 self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
709
710 readline.set_completer( completer )
711 readline.set_completer_delims( " " )
712 readline.parse_and_bind("tab: complete")
713
714 try:
715 readline.read_history_file( self.historyfilename )
716 except IOError:
717 pass # It doesn't exist yet.
718
719 print(__credits__)
720
721 def cleanup( self ):
722 """Write readline history and clean up resources"""
723 debugOut( "writing command history" )
724 try:
725 readline.write_history_file( self.historyfilename )
726 except:
727 print("SHELL: Unable to save command history")
728
729 def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
730 """Register a command"""
731 if usage == "": usage = command
732 if helptext == "": helptext = function.__doc__ or "<not yet documented>"
733 cmds[command] = ( function, numparams, usage, helptext )
734
735 def processCommand( self, command, params ):
736 """Process a command. Check number of params and print a usage string, if appropriate"""
737 debugOut( "processing command '%s'..." % command )
738 try:
739 function, numparams, usage, helptext = cmds[command]
740 except KeyError:
741 print("SHELL: ERROR: '%s' command is not a valid command." % command)
742 self.myout.removeLast()
743 else:
744 if (numparams != -1) and (not len( params ) == numparams):
745 print("Usage: '%s'" % usage)
746 return
747
748 result = function( self.commands, params )
749 debugOut( "result was '%s'" % result )
750
751 def processStartupFile( self ):
752 """Read and execute all commands found in $HOME/.bbsh_startup"""
753 if os.path.exists( self.startupfilename ):
754 startupfile = open( self.startupfilename, "r" )
755 for cmdline in startupfile:
756 debugOut( "processing startup line '%s'" % cmdline )
757 if not cmdline:
758 continue
759 if "|" in cmdline:
760 print("ERROR: '|' in startup file is not allowed. Ignoring line")
761 continue
762 self.commandQ.put( cmdline.strip() )
763
764 def main( self ):
765 """The main command loop"""
766 while not leave_mainloop:
767 try:
768 if self.commandQ.empty():
769 sys.stdout = self.myout.delegate
770 cmdline = raw_input( "BB>> " )
771 sys.stdout = self.myout
772 else:
773 cmdline = self.commandQ.get()
774 if cmdline:
775 allCommands = cmdline.split( ';' )
776 for command in allCommands:
777 pipecmd = None
778 #
779 # special case for expert mode
780 if command == 'python':
781 sys.stdout = self.myout.delegate
782 self.processCommand( command, "" )
783 sys.stdout = self.myout
784 else:
785 self.myout.startCommand( command )
786 if '|' in command: # disable output
787 command, pipecmd = command.split( '|' )
788 delegate = self.myout.delegate
789 self.myout.delegate = None
790 tokens = shlex.split( command, True )
791 self.processCommand( tokens[0], tokens[1:] or "" )
792 self.myout.endCommand()
793 if pipecmd is not None: # restore output
794 self.myout.delegate = delegate
795
796 pipe = popen2.Popen4( pipecmd )
797 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
798 pipe.tochild.close()
799 sys.stdout.write( pipe.fromchild.read() )
800 #
801 except EOFError:
802 print()
803 return
804 except KeyboardInterrupt:
805 print()
806
807##########################################################################
808# Start function - called from the BitBake command line utility
809##########################################################################
810
811def start( aCooker ):
812 global cooker
813 cooker = aCooker
814 bbshell = BitBakeShell()
815 bbshell.processStartupFile()
816 bbshell.main()
817 bbshell.cleanup()
818
819if __name__ == "__main__":
820 print("SHELL: Sorry, this program should only be called by BitBake.")
diff --git a/bitbake/lib/bb/siggen.py b/bitbake/lib/bb/siggen.py
new file mode 100644
index 0000000000..c15ba28ead
--- /dev/null
+++ b/bitbake/lib/bb/siggen.py
@@ -0,0 +1,445 @@
1import hashlib
2import logging
3import os
4import re
5import tempfile
6import bb.data
7
8logger = logging.getLogger('BitBake.SigGen')
9
10try:
11 import cPickle as pickle
12except ImportError:
13 import pickle
14 logger.info('Importing cPickle failed. Falling back to a very slow implementation.')
15
16def init(d):
17 siggens = [obj for obj in globals().itervalues()
18 if type(obj) is type and issubclass(obj, SignatureGenerator)]
19
20 desired = d.getVar("BB_SIGNATURE_HANDLER", True) or "noop"
21 for sg in siggens:
22 if desired == sg.name:
23 return sg(d)
24 break
25 else:
26 logger.error("Invalid signature generator '%s', using default 'noop'\n"
27 "Available generators: %s", desired,
28 ', '.join(obj.name for obj in siggens))
29 return SignatureGenerator(d)
30
31class SignatureGenerator(object):
32 """
33 """
34 name = "noop"
35
36 def __init__(self, data):
37 return
38
39 def finalise(self, fn, d, varient):
40 return
41
42 def get_taskhash(self, fn, task, deps, dataCache):
43 return "0"
44
45 def set_taskdata(self, hashes, deps):
46 return
47
48 def stampfile(self, stampbase, file_name, taskname, extrainfo):
49 return ("%s.%s.%s" % (stampbase, taskname, extrainfo)).rstrip('.')
50
51 def stampcleanmask(self, stampbase, file_name, taskname, extrainfo):
52 return ("%s.%s.%s" % (stampbase, taskname, extrainfo)).rstrip('.')
53
54 def dump_sigtask(self, fn, task, stampbase, runtime):
55 return
56
57 def invalidate_task(self, task, d, fn):
58 bb.build.del_stamp(task, d, fn)
59
60
61class SignatureGeneratorBasic(SignatureGenerator):
62 """
63 """
64 name = "basic"
65
66 def __init__(self, data):
67 self.basehash = {}
68 self.taskhash = {}
69 self.taskdeps = {}
70 self.runtaskdeps = {}
71 self.file_checksum_values = {}
72 self.gendeps = {}
73 self.lookupcache = {}
74 self.pkgnameextract = re.compile("(?P<fn>.*)\..*")
75 self.basewhitelist = set((data.getVar("BB_HASHBASE_WHITELIST", True) or "").split())
76 self.taskwhitelist = None
77 self.init_rundepcheck(data)
78
79 def init_rundepcheck(self, data):
80 self.taskwhitelist = data.getVar("BB_HASHTASK_WHITELIST", True) or None
81 if self.taskwhitelist:
82 self.twl = re.compile(self.taskwhitelist)
83 else:
84 self.twl = None
85
86 def _build_data(self, fn, d):
87
88 tasklist, gendeps, lookupcache = bb.data.generate_dependencies(d)
89
90 taskdeps = {}
91 basehash = {}
92
93 for task in tasklist:
94 data = lookupcache[task]
95
96 if data is None:
97 bb.error("Task %s from %s seems to be empty?!" % (task, fn))
98 data = ''
99
100 gendeps[task] -= self.basewhitelist
101 newdeps = gendeps[task]
102 seen = set()
103 while newdeps:
104 nextdeps = newdeps
105 seen |= nextdeps
106 newdeps = set()
107 for dep in nextdeps:
108 if dep in self.basewhitelist:
109 continue
110 gendeps[dep] -= self.basewhitelist
111 newdeps |= gendeps[dep]
112 newdeps -= seen
113
114 alldeps = sorted(seen)
115 for dep in alldeps:
116 data = data + dep
117 var = lookupcache[dep]
118 if var is not None:
119 data = data + str(var)
120 self.basehash[fn + "." + task] = hashlib.md5(data).hexdigest()
121 taskdeps[task] = alldeps
122
123 self.taskdeps[fn] = taskdeps
124 self.gendeps[fn] = gendeps
125 self.lookupcache[fn] = lookupcache
126
127 return taskdeps
128
129 def finalise(self, fn, d, variant):
130
131 if variant:
132 fn = "virtual:" + variant + ":" + fn
133
134 try:
135 taskdeps = self._build_data(fn, d)
136 except:
137 bb.note("Error during finalise of %s" % fn)
138 raise
139
140 #Slow but can be useful for debugging mismatched basehashes
141 #for task in self.taskdeps[fn]:
142 # self.dump_sigtask(fn, task, d.getVar("STAMP", True), False)
143
144 for task in taskdeps:
145 d.setVar("BB_BASEHASH_task-%s" % task, self.basehash[fn + "." + task])
146
147 def rundep_check(self, fn, recipename, task, dep, depname, dataCache):
148 # Return True if we should keep the dependency, False to drop it
149 # We only manipulate the dependencies for packages not in the whitelist
150 if self.twl and not self.twl.search(recipename):
151 # then process the actual dependencies
152 if self.twl.search(depname):
153 return False
154 return True
155
156 def read_taint(self, fn, task, stampbase):
157 taint = None
158 try:
159 with open(stampbase + '.' + task + '.taint', 'r') as taintf:
160 taint = taintf.read()
161 except IOError:
162 pass
163 return taint
164
165 def get_taskhash(self, fn, task, deps, dataCache):
166 k = fn + "." + task
167 data = dataCache.basetaskhash[k]
168 self.runtaskdeps[k] = []
169 self.file_checksum_values[k] = {}
170 recipename = dataCache.pkg_fn[fn]
171 for dep in sorted(deps, key=clean_basepath):
172 depname = dataCache.pkg_fn[self.pkgnameextract.search(dep).group('fn')]
173 if not self.rundep_check(fn, recipename, task, dep, depname, dataCache):
174 continue
175 if dep not in self.taskhash:
176 bb.fatal("%s is not in taskhash, caller isn't calling in dependency order?", dep)
177 data = data + self.taskhash[dep]
178 self.runtaskdeps[k].append(dep)
179
180 if task in dataCache.file_checksums[fn]:
181 checksums = bb.fetch2.get_file_checksums(dataCache.file_checksums[fn][task], recipename)
182 for (f,cs) in checksums:
183 self.file_checksum_values[k][f] = cs
184 data = data + cs
185
186 taint = self.read_taint(fn, task, dataCache.stamp[fn])
187 if taint:
188 data = data + taint
189
190 h = hashlib.md5(data).hexdigest()
191 self.taskhash[k] = h
192 #d.setVar("BB_TASKHASH_task-%s" % task, taskhash[task])
193 return h
194
195 def set_taskdata(self, hashes, deps, checksums):
196 self.runtaskdeps = deps
197 self.taskhash = hashes
198 self.file_checksum_values = checksums
199
200 def dump_sigtask(self, fn, task, stampbase, runtime):
201 k = fn + "." + task
202 if runtime == "customfile":
203 sigfile = stampbase
204 elif runtime and k in self.taskhash:
205 sigfile = stampbase + "." + task + ".sigdata" + "." + self.taskhash[k]
206 else:
207 sigfile = stampbase + "." + task + ".sigbasedata" + "." + self.basehash[k]
208
209 bb.utils.mkdirhier(os.path.dirname(sigfile))
210
211 data = {}
212 data['basewhitelist'] = self.basewhitelist
213 data['taskwhitelist'] = self.taskwhitelist
214 data['taskdeps'] = self.taskdeps[fn][task]
215 data['basehash'] = self.basehash[k]
216 data['gendeps'] = {}
217 data['varvals'] = {}
218 data['varvals'][task] = self.lookupcache[fn][task]
219 for dep in self.taskdeps[fn][task]:
220 if dep in self.basewhitelist:
221 continue
222 data['gendeps'][dep] = self.gendeps[fn][dep]
223 data['varvals'][dep] = self.lookupcache[fn][dep]
224
225 if runtime and k in self.taskhash:
226 data['runtaskdeps'] = self.runtaskdeps[k]
227 data['file_checksum_values'] = self.file_checksum_values[k]
228 data['runtaskhashes'] = {}
229 for dep in data['runtaskdeps']:
230 data['runtaskhashes'][dep] = self.taskhash[dep]
231
232 taint = self.read_taint(fn, task, stampbase)
233 if taint:
234 data['taint'] = taint
235
236 fd, tmpfile = tempfile.mkstemp(dir=os.path.dirname(sigfile), prefix="sigtask.")
237 try:
238 with os.fdopen(fd, "wb") as stream:
239 p = pickle.dump(data, stream, -1)
240 stream.flush()
241 os.fsync(fd)
242 os.chmod(tmpfile, 0664)
243 os.rename(tmpfile, sigfile)
244 except (OSError, IOError) as err:
245 try:
246 os.unlink(tmpfile)
247 except OSError:
248 pass
249 raise err
250
251 def dump_sigs(self, dataCache):
252 for fn in self.taskdeps:
253 for task in self.taskdeps[fn]:
254 k = fn + "." + task
255 if k not in self.taskhash:
256 continue
257 if dataCache.basetaskhash[k] != self.basehash[k]:
258 bb.error("Bitbake's cached basehash does not match the one we just generated (%s)!" % k)
259 bb.error("The mismatched hashes were %s and %s" % (dataCache.basetaskhash[k], self.basehash[k]))
260 self.dump_sigtask(fn, task, dataCache.stamp[fn], True)
261
262class SignatureGeneratorBasicHash(SignatureGeneratorBasic):
263 name = "basichash"
264
265 def stampfile(self, stampbase, fn, taskname, extrainfo, clean=False):
266 if taskname != "do_setscene" and taskname.endswith("_setscene"):
267 k = fn + "." + taskname[:-9]
268 else:
269 k = fn + "." + taskname
270 if clean:
271 h = "*"
272 elif k in self.taskhash:
273 h = self.taskhash[k]
274 else:
275 # If k is not in basehash, then error
276 h = self.basehash[k]
277 return ("%s.%s.%s.%s" % (stampbase, taskname, h, extrainfo)).rstrip('.')
278
279 def stampcleanmask(self, stampbase, fn, taskname, extrainfo):
280 return self.stampfile(stampbase, fn, taskname, extrainfo, clean=True)
281
282 def invalidate_task(self, task, d, fn):
283 bb.note("Tainting hash to force rebuild of task %s, %s" % (fn, task))
284 bb.build.write_taint(task, d, fn)
285
286def dump_this_task(outfile, d):
287 import bb.parse
288 fn = d.getVar("BB_FILENAME", True)
289 task = "do_" + d.getVar("BB_CURRENTTASK", True)
290 bb.parse.siggen.dump_sigtask(fn, task, outfile, "customfile")
291
292def clean_basepath(a):
293 if a.startswith("virtual:"):
294 b = a.rsplit(":", 1)[0] + ":" + a.rsplit("/", 1)[1]
295 else:
296 b = a.rsplit("/", 1)[1]
297 return b
298
299def clean_basepaths(a):
300 b = {}
301 for x in a:
302 b[clean_basepath(x)] = a[x]
303 return b
304
305def compare_sigfiles(a, b, recursecb = None):
306 output = []
307
308 p1 = pickle.Unpickler(open(a, "rb"))
309 a_data = p1.load()
310 p2 = pickle.Unpickler(open(b, "rb"))
311 b_data = p2.load()
312
313 def dict_diff(a, b, whitelist=set()):
314 sa = set(a.keys())
315 sb = set(b.keys())
316 common = sa & sb
317 changed = set()
318 for i in common:
319 if a[i] != b[i] and i not in whitelist:
320 changed.add(i)
321 added = sa - sb
322 removed = sb - sa
323 return changed, added, removed
324
325 if 'basewhitelist' in a_data and a_data['basewhitelist'] != b_data['basewhitelist']:
326 output.append("basewhitelist changed from '%s' to '%s'" % (a_data['basewhitelist'], b_data['basewhitelist']))
327 if a_data['basewhitelist'] and b_data['basewhitelist']:
328 output.append("changed items: %s" % a_data['basewhitelist'].symmetric_difference(b_data['basewhitelist']))
329
330 if 'taskwhitelist' in a_data and a_data['taskwhitelist'] != b_data['taskwhitelist']:
331 output.append("taskwhitelist changed from '%s' to '%s'" % (a_data['taskwhitelist'], b_data['taskwhitelist']))
332 if a_data['taskwhitelist'] and b_data['taskwhitelist']:
333 output.append("changed items: %s" % a_data['taskwhitelist'].symmetric_difference(b_data['taskwhitelist']))
334
335 if a_data['taskdeps'] != b_data['taskdeps']:
336 output.append("Task dependencies changed from:\n%s\nto:\n%s" % (sorted(a_data['taskdeps']), sorted(b_data['taskdeps'])))
337
338 if a_data['basehash'] != b_data['basehash']:
339 output.append("basehash changed from %s to %s" % (a_data['basehash'], b_data['basehash']))
340
341 changed, added, removed = dict_diff(a_data['gendeps'], b_data['gendeps'], a_data['basewhitelist'] & b_data['basewhitelist'])
342 if changed:
343 for dep in changed:
344 output.append("List of dependencies for variable %s changed from '%s' to '%s'" % (dep, a_data['gendeps'][dep], b_data['gendeps'][dep]))
345 if a_data['gendeps'][dep] and b_data['gendeps'][dep]:
346 output.append("changed items: %s" % a_data['gendeps'][dep].symmetric_difference(b_data['gendeps'][dep]))
347 if added:
348 for dep in added:
349 output.append("Dependency on variable %s was added" % (dep))
350 if removed:
351 for dep in removed:
352 output.append("Dependency on Variable %s was removed" % (dep))
353
354
355 changed, added, removed = dict_diff(a_data['varvals'], b_data['varvals'])
356 if changed:
357 for dep in changed:
358 output.append("Variable %s value changed from '%s' to '%s'" % (dep, a_data['varvals'][dep], b_data['varvals'][dep]))
359
360 changed, added, removed = dict_diff(a_data['file_checksum_values'], b_data['file_checksum_values'])
361 if changed:
362 for f in changed:
363 output.append("Checksum for file %s changed from %s to %s" % (f, a_data['file_checksum_values'][f], b_data['file_checksum_values'][f]))
364 if added:
365 for f in added:
366 output.append("Dependency on checksum of file %s was added" % (f))
367 if removed:
368 for f in removed:
369 output.append("Dependency on checksum of file %s was removed" % (f))
370
371
372 if 'runtaskhashes' in a_data and 'runtaskhashes' in b_data:
373 a = a_data['runtaskhashes']
374 b = b_data['runtaskhashes']
375 changed, added, removed = dict_diff(a, b)
376 if added:
377 for dep in added:
378 bdep_found = False
379 if removed:
380 for bdep in removed:
381 if a[dep] == b[bdep]:
382 #output.append("Dependency on task %s was replaced by %s with same hash" % (dep, bdep))
383 bdep_found = True
384 if not bdep_found:
385 output.append("Dependency on task %s was added with hash %s" % (clean_basepath(dep), a[dep]))
386 if removed:
387 for dep in removed:
388 adep_found = False
389 if added:
390 for adep in added:
391 if a[adep] == b[dep]:
392 #output.append("Dependency on task %s was replaced by %s with same hash" % (adep, dep))
393 adep_found = True
394 if not adep_found:
395 output.append("Dependency on task %s was removed with hash %s" % (clean_basepath(dep), b[dep]))
396 if changed:
397 for dep in changed:
398 output.append("Hash for dependent task %s changed from %s to %s" % (clean_basepath(dep), a[dep], b[dep]))
399 if callable(recursecb):
400 recout = recursecb(dep, a[dep], b[dep])
401 if recout:
402 output.extend(recout)
403
404 a_taint = a_data.get('taint', None)
405 b_taint = b_data.get('taint', None)
406 if a_taint != b_taint:
407 output.append("Taint (by forced/invalidated task) changed from %s to %s" % (a_taint, b_taint))
408
409 return output
410
411
412def dump_sigfile(a):
413 output = []
414
415 p1 = pickle.Unpickler(open(a, "rb"))
416 a_data = p1.load()
417
418 output.append("basewhitelist: %s" % (a_data['basewhitelist']))
419
420 output.append("taskwhitelist: %s" % (a_data['taskwhitelist']))
421
422 output.append("Task dependencies: %s" % (sorted(a_data['taskdeps'])))
423
424 output.append("basehash: %s" % (a_data['basehash']))
425
426 for dep in a_data['gendeps']:
427 output.append("List of dependencies for variable %s is %s" % (dep, a_data['gendeps'][dep]))
428
429 for dep in a_data['varvals']:
430 output.append("Variable %s value is %s" % (dep, a_data['varvals'][dep]))
431
432 if 'runtaskdeps' in a_data:
433 output.append("Tasks this task depends on: %s" % (a_data['runtaskdeps']))
434
435 if 'file_checksum_values' in a_data:
436 output.append("This task depends on the checksums of files: %s" % (a_data['file_checksum_values']))
437
438 if 'runtaskhashes' in a_data:
439 for dep in a_data['runtaskhashes']:
440 output.append("Hash for dependent task %s is %s" % (dep, a_data['runtaskhashes'][dep]))
441
442 if 'taint' in a_data:
443 output.append("Tainted (by forced/invalidated task): %s" % a_data['taint'])
444
445 return output
diff --git a/bitbake/lib/bb/taskdata.py b/bitbake/lib/bb/taskdata.py
new file mode 100644
index 0000000000..58fe1995f2
--- /dev/null
+++ b/bitbake/lib/bb/taskdata.py
@@ -0,0 +1,645 @@
1#!/usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4"""
5BitBake 'TaskData' implementation
6
7Task data collection and handling
8
9"""
10
11# Copyright (C) 2006 Richard Purdie
12#
13# This program is free software; you can redistribute it and/or modify
14# it under the terms of the GNU General Public License version 2 as
15# published by the Free Software Foundation.
16#
17# This program is distributed in the hope that it will be useful,
18# but WITHOUT ANY WARRANTY; without even the implied warranty of
19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20# GNU General Public License for more details.
21#
22# You should have received a copy of the GNU General Public License along
23# with this program; if not, write to the Free Software Foundation, Inc.,
24# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25
26import logging
27import re
28import bb
29
30logger = logging.getLogger("BitBake.TaskData")
31
32def re_match_strings(target, strings):
33 """
34 Whether or not the string 'target' matches
35 any one string of the strings which can be regular expression string
36 """
37 return any(name == target or re.match(name, target)
38 for name in strings)
39
40class TaskData:
41 """
42 BitBake Task Data implementation
43 """
44 def __init__(self, abort = True, tryaltconfigs = False, skiplist = None):
45 self.build_names_index = []
46 self.run_names_index = []
47 self.fn_index = []
48
49 self.build_targets = {}
50 self.run_targets = {}
51
52 self.external_targets = []
53
54 self.tasks_fnid = []
55 self.tasks_name = []
56 self.tasks_tdepends = []
57 self.tasks_idepends = []
58 self.tasks_irdepends = []
59 # Cache to speed up task ID lookups
60 self.tasks_lookup = {}
61
62 self.depids = {}
63 self.rdepids = {}
64
65 self.consider_msgs_cache = []
66
67 self.failed_deps = []
68 self.failed_rdeps = []
69 self.failed_fnids = []
70
71 self.abort = abort
72 self.tryaltconfigs = tryaltconfigs
73
74 self.skiplist = skiplist
75
76 def getbuild_id(self, name):
77 """
78 Return an ID number for the build target name.
79 If it doesn't exist, create one.
80 """
81 if not name in self.build_names_index:
82 self.build_names_index.append(name)
83 return len(self.build_names_index) - 1
84
85 return self.build_names_index.index(name)
86
87 def getrun_id(self, name):
88 """
89 Return an ID number for the run target name.
90 If it doesn't exist, create one.
91 """
92 if not name in self.run_names_index:
93 self.run_names_index.append(name)
94 return len(self.run_names_index) - 1
95
96 return self.run_names_index.index(name)
97
98 def getfn_id(self, name):
99 """
100 Return an ID number for the filename.
101 If it doesn't exist, create one.
102 """
103 if not name in self.fn_index:
104 self.fn_index.append(name)
105 return len(self.fn_index) - 1
106
107 return self.fn_index.index(name)
108
109 def gettask_ids(self, fnid):
110 """
111 Return an array of the ID numbers matching a given fnid.
112 """
113 ids = []
114 if fnid in self.tasks_lookup:
115 for task in self.tasks_lookup[fnid]:
116 ids.append(self.tasks_lookup[fnid][task])
117 return ids
118
119 def gettask_id_fromfnid(self, fnid, task):
120 """
121 Return an ID number for the task matching fnid and task.
122 """
123 if fnid in self.tasks_lookup:
124 if task in self.tasks_lookup[fnid]:
125 return self.tasks_lookup[fnid][task]
126
127 return None
128
129 def gettask_id(self, fn, task, create = True):
130 """
131 Return an ID number for the task matching fn and task.
132 If it doesn't exist, create one by default.
133 Optionally return None instead.
134 """
135 fnid = self.getfn_id(fn)
136
137 if fnid in self.tasks_lookup:
138 if task in self.tasks_lookup[fnid]:
139 return self.tasks_lookup[fnid][task]
140
141 if not create:
142 return None
143
144 self.tasks_name.append(task)
145 self.tasks_fnid.append(fnid)
146 self.tasks_tdepends.append([])
147 self.tasks_idepends.append([])
148 self.tasks_irdepends.append([])
149
150 listid = len(self.tasks_name) - 1
151
152 if fnid not in self.tasks_lookup:
153 self.tasks_lookup[fnid] = {}
154 self.tasks_lookup[fnid][task] = listid
155
156 return listid
157
158 def add_tasks(self, fn, dataCache):
159 """
160 Add tasks for a given fn to the database
161 """
162
163 task_deps = dataCache.task_deps[fn]
164
165 fnid = self.getfn_id(fn)
166
167 if fnid in self.failed_fnids:
168 bb.msg.fatal("TaskData", "Trying to re-add a failed file? Something is broken...")
169
170 # Check if we've already seen this fn
171 if fnid in self.tasks_fnid:
172 return
173
174 for task in task_deps['tasks']:
175
176 # Work out task dependencies
177 parentids = []
178 for dep in task_deps['parents'][task]:
179 if dep not in task_deps['tasks']:
180 bb.debug(2, "Not adding dependeny of %s on %s since %s does not exist" % (task, dep, dep))
181 continue
182 parentid = self.gettask_id(fn, dep)
183 parentids.append(parentid)
184 taskid = self.gettask_id(fn, task)
185 self.tasks_tdepends[taskid].extend(parentids)
186
187 # Touch all intertask dependencies
188 if 'depends' in task_deps and task in task_deps['depends']:
189 ids = []
190 for dep in task_deps['depends'][task].split():
191 if dep:
192 if ":" not in dep:
193 bb.msg.fatal("TaskData", "Error for %s, dependency %s does not contain ':' character\n. Task 'depends' should be specified in the form 'packagename:task'" % (fn, dep))
194 ids.append(((self.getbuild_id(dep.split(":")[0])), dep.split(":")[1]))
195 self.tasks_idepends[taskid].extend(ids)
196 if 'rdepends' in task_deps and task in task_deps['rdepends']:
197 ids = []
198 for dep in task_deps['rdepends'][task].split():
199 if dep:
200 if ":" not in dep:
201 bb.msg.fatal("TaskData", "Error for %s, dependency %s does not contain ':' character\n. Task 'rdepends' should be specified in the form 'packagename:task'" % (fn, dep))
202 ids.append(((self.getrun_id(dep.split(":")[0])), dep.split(":")[1]))
203 self.tasks_irdepends[taskid].extend(ids)
204
205
206 # Work out build dependencies
207 if not fnid in self.depids:
208 dependids = {}
209 for depend in dataCache.deps[fn]:
210 logger.debug(2, "Added dependency %s for %s", depend, fn)
211 dependids[self.getbuild_id(depend)] = None
212 self.depids[fnid] = dependids.keys()
213
214 # Work out runtime dependencies
215 if not fnid in self.rdepids:
216 rdependids = {}
217 rdepends = dataCache.rundeps[fn]
218 rrecs = dataCache.runrecs[fn]
219 for package in rdepends:
220 for rdepend in rdepends[package]:
221 logger.debug(2, "Added runtime dependency %s for %s", rdepend, fn)
222 rdependids[self.getrun_id(rdepend)] = None
223 for package in rrecs:
224 for rdepend in rrecs[package]:
225 logger.debug(2, "Added runtime recommendation %s for %s", rdepend, fn)
226 rdependids[self.getrun_id(rdepend)] = None
227 self.rdepids[fnid] = rdependids.keys()
228
229 for dep in self.depids[fnid]:
230 if dep in self.failed_deps:
231 self.fail_fnid(fnid)
232 return
233 for dep in self.rdepids[fnid]:
234 if dep in self.failed_rdeps:
235 self.fail_fnid(fnid)
236 return
237
238 def have_build_target(self, target):
239 """
240 Have we a build target matching this name?
241 """
242 targetid = self.getbuild_id(target)
243
244 if targetid in self.build_targets:
245 return True
246 return False
247
248 def have_runtime_target(self, target):
249 """
250 Have we a runtime target matching this name?
251 """
252 targetid = self.getrun_id(target)
253
254 if targetid in self.run_targets:
255 return True
256 return False
257
258 def add_build_target(self, fn, item):
259 """
260 Add a build target.
261 If already present, append the provider fn to the list
262 """
263 targetid = self.getbuild_id(item)
264 fnid = self.getfn_id(fn)
265
266 if targetid in self.build_targets:
267 if fnid in self.build_targets[targetid]:
268 return
269 self.build_targets[targetid].append(fnid)
270 return
271 self.build_targets[targetid] = [fnid]
272
273 def add_runtime_target(self, fn, item):
274 """
275 Add a runtime target.
276 If already present, append the provider fn to the list
277 """
278 targetid = self.getrun_id(item)
279 fnid = self.getfn_id(fn)
280
281 if targetid in self.run_targets:
282 if fnid in self.run_targets[targetid]:
283 return
284 self.run_targets[targetid].append(fnid)
285 return
286 self.run_targets[targetid] = [fnid]
287
288 def mark_external_target(self, item):
289 """
290 Mark a build target as being externally requested
291 """
292 targetid = self.getbuild_id(item)
293
294 if targetid not in self.external_targets:
295 self.external_targets.append(targetid)
296
297 def get_unresolved_build_targets(self, dataCache):
298 """
299 Return a list of build targets who's providers
300 are unknown.
301 """
302 unresolved = []
303 for target in self.build_names_index:
304 if re_match_strings(target, dataCache.ignored_dependencies):
305 continue
306 if self.build_names_index.index(target) in self.failed_deps:
307 continue
308 if not self.have_build_target(target):
309 unresolved.append(target)
310 return unresolved
311
312 def get_unresolved_run_targets(self, dataCache):
313 """
314 Return a list of runtime targets who's providers
315 are unknown.
316 """
317 unresolved = []
318 for target in self.run_names_index:
319 if re_match_strings(target, dataCache.ignored_dependencies):
320 continue
321 if self.run_names_index.index(target) in self.failed_rdeps:
322 continue
323 if not self.have_runtime_target(target):
324 unresolved.append(target)
325 return unresolved
326
327 def get_provider(self, item):
328 """
329 Return a list of providers of item
330 """
331 targetid = self.getbuild_id(item)
332
333 return self.build_targets[targetid]
334
335 def get_dependees(self, itemid):
336 """
337 Return a list of targets which depend on item
338 """
339 dependees = []
340 for fnid in self.depids:
341 if itemid in self.depids[fnid]:
342 dependees.append(fnid)
343 return dependees
344
345 def get_dependees_str(self, item):
346 """
347 Return a list of targets which depend on item as a user readable string
348 """
349 itemid = self.getbuild_id(item)
350 dependees = []
351 for fnid in self.depids:
352 if itemid in self.depids[fnid]:
353 dependees.append(self.fn_index[fnid])
354 return dependees
355
356 def get_rdependees(self, itemid):
357 """
358 Return a list of targets which depend on runtime item
359 """
360 dependees = []
361 for fnid in self.rdepids:
362 if itemid in self.rdepids[fnid]:
363 dependees.append(fnid)
364 return dependees
365
366 def get_rdependees_str(self, item):
367 """
368 Return a list of targets which depend on runtime item as a user readable string
369 """
370 itemid = self.getrun_id(item)
371 dependees = []
372 for fnid in self.rdepids:
373 if itemid in self.rdepids[fnid]:
374 dependees.append(self.fn_index[fnid])
375 return dependees
376
377 def get_reasons(self, item, runtime=False):
378 """
379 Get the reason(s) for an item not being provided, if any
380 """
381 reasons = []
382 if self.skiplist:
383 for fn in self.skiplist:
384 skipitem = self.skiplist[fn]
385 if skipitem.pn == item:
386 reasons.append("%s was skipped: %s" % (skipitem.pn, skipitem.skipreason))
387 elif runtime and item in skipitem.rprovides:
388 reasons.append("%s RPROVIDES %s but was skipped: %s" % (skipitem.pn, item, skipitem.skipreason))
389 elif not runtime and item in skipitem.provides:
390 reasons.append("%s PROVIDES %s but was skipped: %s" % (skipitem.pn, item, skipitem.skipreason))
391 return reasons
392
393 def get_close_matches(self, item, provider_list):
394 import difflib
395 if self.skiplist:
396 skipped = []
397 for fn in self.skiplist:
398 skipped.append(self.skiplist[fn].pn)
399 full_list = provider_list + skipped
400 else:
401 full_list = provider_list
402 return difflib.get_close_matches(item, full_list, cutoff=0.7)
403
404 def add_provider(self, cfgData, dataCache, item):
405 try:
406 self.add_provider_internal(cfgData, dataCache, item)
407 except bb.providers.NoProvider:
408 if self.abort:
409 raise
410 self.remove_buildtarget(self.getbuild_id(item))
411
412 self.mark_external_target(item)
413
414 def add_provider_internal(self, cfgData, dataCache, item):
415 """
416 Add the providers of item to the task data
417 Mark entries were specifically added externally as against dependencies
418 added internally during dependency resolution
419 """
420
421 if re_match_strings(item, dataCache.ignored_dependencies):
422 return
423
424 if not item in dataCache.providers:
425 bb.event.fire(bb.event.NoProvider(item, dependees=self.get_dependees_str(item), reasons=self.get_reasons(item), close_matches=self.get_close_matches(item, dataCache.providers.keys())), cfgData)
426 raise bb.providers.NoProvider(item)
427
428 if self.have_build_target(item):
429 return
430
431 all_p = dataCache.providers[item]
432
433 eligible, foundUnique = bb.providers.filterProviders(all_p, item, cfgData, dataCache)
434 eligible = [p for p in eligible if not self.getfn_id(p) in self.failed_fnids]
435
436 if not eligible:
437 bb.event.fire(bb.event.NoProvider(item, dependees=self.get_dependees_str(item), reasons=["No eligible PROVIDERs exist for '%s'" % item]), cfgData)
438 raise bb.providers.NoProvider(item)
439
440 if len(eligible) > 1 and foundUnique == False:
441 if item not in self.consider_msgs_cache:
442 providers_list = []
443 for fn in eligible:
444 providers_list.append(dataCache.pkg_fn[fn])
445 bb.event.fire(bb.event.MultipleProviders(item, providers_list), cfgData)
446 self.consider_msgs_cache.append(item)
447
448 for fn in eligible:
449 fnid = self.getfn_id(fn)
450 if fnid in self.failed_fnids:
451 continue
452 logger.debug(2, "adding %s to satisfy %s", fn, item)
453 self.add_build_target(fn, item)
454 self.add_tasks(fn, dataCache)
455
456
457 #item = dataCache.pkg_fn[fn]
458
459 def add_rprovider(self, cfgData, dataCache, item):
460 """
461 Add the runtime providers of item to the task data
462 (takes item names from RDEPENDS/PACKAGES namespace)
463 """
464
465 if re_match_strings(item, dataCache.ignored_dependencies):
466 return
467
468 if self.have_runtime_target(item):
469 return
470
471 all_p = bb.providers.getRuntimeProviders(dataCache, item)
472
473 if not all_p:
474 bb.event.fire(bb.event.NoProvider(item, runtime=True, dependees=self.get_rdependees_str(item), reasons=self.get_reasons(item, True)), cfgData)
475 raise bb.providers.NoRProvider(item)
476
477 eligible, numberPreferred = bb.providers.filterProvidersRunTime(all_p, item, cfgData, dataCache)
478 eligible = [p for p in eligible if not self.getfn_id(p) in self.failed_fnids]
479
480 if not eligible:
481 bb.event.fire(bb.event.NoProvider(item, runtime=True, dependees=self.get_rdependees_str(item), reasons=["No eligible RPROVIDERs exist for '%s'" % item]), cfgData)
482 raise bb.providers.NoRProvider(item)
483
484 if len(eligible) > 1 and numberPreferred == 0:
485 if item not in self.consider_msgs_cache:
486 providers_list = []
487 for fn in eligible:
488 providers_list.append(dataCache.pkg_fn[fn])
489 bb.event.fire(bb.event.MultipleProviders(item, providers_list, runtime=True), cfgData)
490 self.consider_msgs_cache.append(item)
491
492 if numberPreferred > 1:
493 if item not in self.consider_msgs_cache:
494 providers_list = []
495 for fn in eligible:
496 providers_list.append(dataCache.pkg_fn[fn])
497 bb.event.fire(bb.event.MultipleProviders(item, providers_list, runtime=True), cfgData)
498 self.consider_msgs_cache.append(item)
499 raise bb.providers.MultipleRProvider(item)
500
501 # run through the list until we find one that we can build
502 for fn in eligible:
503 fnid = self.getfn_id(fn)
504 if fnid in self.failed_fnids:
505 continue
506 logger.debug(2, "adding '%s' to satisfy runtime '%s'", fn, item)
507 self.add_runtime_target(fn, item)
508 self.add_tasks(fn, dataCache)
509
510 def fail_fnid(self, fnid, missing_list = []):
511 """
512 Mark a file as failed (unbuildable)
513 Remove any references from build and runtime provider lists
514
515 missing_list, A list of missing requirements for this target
516 """
517 if fnid in self.failed_fnids:
518 return
519 logger.debug(1, "File '%s' is unbuildable, removing...", self.fn_index[fnid])
520 self.failed_fnids.append(fnid)
521 for target in self.build_targets:
522 if fnid in self.build_targets[target]:
523 self.build_targets[target].remove(fnid)
524 if len(self.build_targets[target]) == 0:
525 self.remove_buildtarget(target, missing_list)
526 for target in self.run_targets:
527 if fnid in self.run_targets[target]:
528 self.run_targets[target].remove(fnid)
529 if len(self.run_targets[target]) == 0:
530 self.remove_runtarget(target, missing_list)
531
532 def remove_buildtarget(self, targetid, missing_list = []):
533 """
534 Mark a build target as failed (unbuildable)
535 Trigger removal of any files that have this as a dependency
536 """
537 if not missing_list:
538 missing_list = [self.build_names_index[targetid]]
539 else:
540 missing_list = [self.build_names_index[targetid]] + missing_list
541 logger.verbose("Target '%s' is unbuildable, removing...\nMissing or unbuildable dependency chain was: %s", self.build_names_index[targetid], missing_list)
542 self.failed_deps.append(targetid)
543 dependees = self.get_dependees(targetid)
544 for fnid in dependees:
545 self.fail_fnid(fnid, missing_list)
546 for taskid in xrange(len(self.tasks_idepends)):
547 idepends = self.tasks_idepends[taskid]
548 for (idependid, idependtask) in idepends:
549 if idependid == targetid:
550 self.fail_fnid(self.tasks_fnid[taskid], missing_list)
551
552 if self.abort and targetid in self.external_targets:
553 target = self.build_names_index[targetid]
554 logger.error("Required build target '%s' has no buildable providers.\nMissing or unbuildable dependency chain was: %s", target, missing_list)
555 raise bb.providers.NoProvider(target)
556
557 def remove_runtarget(self, targetid, missing_list = []):
558 """
559 Mark a run target as failed (unbuildable)
560 Trigger removal of any files that have this as a dependency
561 """
562 if not missing_list:
563 missing_list = [self.run_names_index[targetid]]
564 else:
565 missing_list = [self.run_names_index[targetid]] + missing_list
566
567 logger.info("Runtime target '%s' is unbuildable, removing...\nMissing or unbuildable dependency chain was: %s", self.run_names_index[targetid], missing_list)
568 self.failed_rdeps.append(targetid)
569 dependees = self.get_rdependees(targetid)
570 for fnid in dependees:
571 self.fail_fnid(fnid, missing_list)
572 for taskid in xrange(len(self.tasks_irdepends)):
573 irdepends = self.tasks_irdepends[taskid]
574 for (idependid, idependtask) in irdepends:
575 if idependid == targetid:
576 self.fail_fnid(self.tasks_fnid[taskid], missing_list)
577
578 def add_unresolved(self, cfgData, dataCache):
579 """
580 Resolve all unresolved build and runtime targets
581 """
582 logger.info("Resolving any missing task queue dependencies")
583 while True:
584 added = 0
585 for target in self.get_unresolved_build_targets(dataCache):
586 try:
587 self.add_provider_internal(cfgData, dataCache, target)
588 added = added + 1
589 except bb.providers.NoProvider:
590 targetid = self.getbuild_id(target)
591 if self.abort and targetid in self.external_targets:
592 raise
593 self.remove_buildtarget(targetid)
594 for target in self.get_unresolved_run_targets(dataCache):
595 try:
596 self.add_rprovider(cfgData, dataCache, target)
597 added = added + 1
598 except (bb.providers.NoRProvider, bb.providers.MultipleRProvider):
599 self.remove_runtarget(self.getrun_id(target))
600 logger.debug(1, "Resolved " + str(added) + " extra dependencies")
601 if added == 0:
602 break
603 # self.dump_data()
604
605 def dump_data(self):
606 """
607 Dump some debug information on the internal data structures
608 """
609 logger.debug(3, "build_names:")
610 logger.debug(3, ", ".join(self.build_names_index))
611
612 logger.debug(3, "run_names:")
613 logger.debug(3, ", ".join(self.run_names_index))
614
615 logger.debug(3, "build_targets:")
616 for buildid in xrange(len(self.build_names_index)):
617 target = self.build_names_index[buildid]
618 targets = "None"
619 if buildid in self.build_targets:
620 targets = self.build_targets[buildid]
621 logger.debug(3, " (%s)%s: %s", buildid, target, targets)
622
623 logger.debug(3, "run_targets:")
624 for runid in xrange(len(self.run_names_index)):
625 target = self.run_names_index[runid]
626 targets = "None"
627 if runid in self.run_targets:
628 targets = self.run_targets[runid]
629 logger.debug(3, " (%s)%s: %s", runid, target, targets)
630
631 logger.debug(3, "tasks:")
632 for task in xrange(len(self.tasks_name)):
633 logger.debug(3, " (%s)%s - %s: %s",
634 task,
635 self.fn_index[self.tasks_fnid[task]],
636 self.tasks_name[task],
637 self.tasks_tdepends[task])
638
639 logger.debug(3, "dependency ids (per fn):")
640 for fnid in self.depids:
641 logger.debug(3, " %s %s: %s", fnid, self.fn_index[fnid], self.depids[fnid])
642
643 logger.debug(3, "runtime dependency ids (per fn):")
644 for fnid in self.rdepids:
645 logger.debug(3, " %s %s: %s", fnid, self.fn_index[fnid], self.rdepids[fnid])
diff --git a/bitbake/lib/bb/tests/__init__.py b/bitbake/lib/bb/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/bitbake/lib/bb/tests/__init__.py
diff --git a/bitbake/lib/bb/tests/codeparser.py b/bitbake/lib/bb/tests/codeparser.py
new file mode 100644
index 0000000000..938b04b2c6
--- /dev/null
+++ b/bitbake/lib/bb/tests/codeparser.py
@@ -0,0 +1,374 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake Test for codeparser.py
5#
6# Copyright (C) 2010 Chris Larson
7# Copyright (C) 2012 Richard Purdie
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21#
22
23import unittest
24import logging
25import bb
26
27logger = logging.getLogger('BitBake.TestCodeParser')
28
29# bb.data references bb.parse but can't directly import due to circular dependencies.
30# Hack around it for now :(
31import bb.parse
32import bb.data
33
34class ReferenceTest(unittest.TestCase):
35 def setUp(self):
36 self.d = bb.data.init()
37
38 def setEmptyVars(self, varlist):
39 for k in varlist:
40 self.d.setVar(k, "")
41
42 def setValues(self, values):
43 for k, v in values.items():
44 self.d.setVar(k, v)
45
46 def assertReferences(self, refs):
47 self.assertEqual(self.references, refs)
48
49 def assertExecs(self, execs):
50 self.assertEqual(self.execs, execs)
51
52class VariableReferenceTest(ReferenceTest):
53
54 def parseExpression(self, exp):
55 parsedvar = self.d.expandWithRefs(exp, None)
56 self.references = parsedvar.references
57
58 def test_simple_reference(self):
59 self.setEmptyVars(["FOO"])
60 self.parseExpression("${FOO}")
61 self.assertReferences(set(["FOO"]))
62
63 def test_nested_reference(self):
64 self.setEmptyVars(["BAR"])
65 self.d.setVar("FOO", "BAR")
66 self.parseExpression("${${FOO}}")
67 self.assertReferences(set(["FOO", "BAR"]))
68
69 def test_python_reference(self):
70 self.setEmptyVars(["BAR"])
71 self.parseExpression("${@bb.data.getVar('BAR', d, True) + 'foo'}")
72 self.assertReferences(set(["BAR"]))
73
74class ShellReferenceTest(ReferenceTest):
75
76 def parseExpression(self, exp):
77 parsedvar = self.d.expandWithRefs(exp, None)
78 parser = bb.codeparser.ShellParser("ParserTest", logger)
79 parser.parse_shell(parsedvar.value)
80
81 self.references = parsedvar.references
82 self.execs = parser.execs
83
84 def test_quotes_inside_assign(self):
85 self.parseExpression('foo=foo"bar"baz')
86 self.assertReferences(set([]))
87
88 def test_quotes_inside_arg(self):
89 self.parseExpression('sed s#"bar baz"#"alpha beta"#g')
90 self.assertExecs(set(["sed"]))
91
92 def test_arg_continuation(self):
93 self.parseExpression("sed -i -e s,foo,bar,g \\\n *.pc")
94 self.assertExecs(set(["sed"]))
95
96 def test_dollar_in_quoted(self):
97 self.parseExpression('sed -i -e "foo$" *.pc')
98 self.assertExecs(set(["sed"]))
99
100 def test_quotes_inside_arg_continuation(self):
101 self.setEmptyVars(["bindir", "D", "libdir"])
102 self.parseExpression("""
103sed -i -e s#"moc_location=.*$"#"moc_location=${bindir}/moc4"# \\
104-e s#"uic_location=.*$"#"uic_location=${bindir}/uic4"# \\
105${D}${libdir}/pkgconfig/*.pc
106""")
107 self.assertReferences(set(["bindir", "D", "libdir"]))
108
109 def test_assign_subshell_expansion(self):
110 self.parseExpression("foo=$(echo bar)")
111 self.assertExecs(set(["echo"]))
112
113 def test_shell_unexpanded(self):
114 self.setEmptyVars(["QT_BASE_NAME"])
115 self.parseExpression('echo "${QT_BASE_NAME}"')
116 self.assertExecs(set(["echo"]))
117 self.assertReferences(set(["QT_BASE_NAME"]))
118
119 def test_incomplete_varexp_single_quotes(self):
120 self.parseExpression("sed -i -e 's:IP{:I${:g' $pc")
121 self.assertExecs(set(["sed"]))
122
123
124 def test_until(self):
125 self.parseExpression("until false; do echo true; done")
126 self.assertExecs(set(["false", "echo"]))
127 self.assertReferences(set())
128
129 def test_case(self):
130 self.parseExpression("""
131case $foo in
132*)
133bar
134;;
135esac
136""")
137 self.assertExecs(set(["bar"]))
138 self.assertReferences(set())
139
140 def test_assign_exec(self):
141 self.parseExpression("a=b c='foo bar' alpha 1 2 3")
142 self.assertExecs(set(["alpha"]))
143
144 def test_redirect_to_file(self):
145 self.setEmptyVars(["foo"])
146 self.parseExpression("echo foo >${foo}/bar")
147 self.assertExecs(set(["echo"]))
148 self.assertReferences(set(["foo"]))
149
150 def test_heredoc(self):
151 self.setEmptyVars(["theta"])
152 self.parseExpression("""
153cat <<END
154alpha
155beta
156${theta}
157END
158""")
159 self.assertReferences(set(["theta"]))
160
161 def test_redirect_from_heredoc(self):
162 v = ["B", "SHADOW_MAILDIR", "SHADOW_MAILFILE", "SHADOW_UTMPDIR", "SHADOW_LOGDIR", "bindir"]
163 self.setEmptyVars(v)
164 self.parseExpression("""
165cat <<END >${B}/cachedpaths
166shadow_cv_maildir=${SHADOW_MAILDIR}
167shadow_cv_mailfile=${SHADOW_MAILFILE}
168shadow_cv_utmpdir=${SHADOW_UTMPDIR}
169shadow_cv_logdir=${SHADOW_LOGDIR}
170shadow_cv_passwd_dir=${bindir}
171END
172""")
173 self.assertReferences(set(v))
174 self.assertExecs(set(["cat"]))
175
176# def test_incomplete_command_expansion(self):
177# self.assertRaises(reftracker.ShellSyntaxError, reftracker.execs,
178# bbvalue.shparse("cp foo`", self.d), self.d)
179
180# def test_rogue_dollarsign(self):
181# self.setValues({"D" : "/tmp"})
182# self.parseExpression("install -d ${D}$")
183# self.assertReferences(set(["D"]))
184# self.assertExecs(set(["install"]))
185
186
187class PythonReferenceTest(ReferenceTest):
188
189 def setUp(self):
190 self.d = bb.data.init()
191 if hasattr(bb.utils, "_context"):
192 self.context = bb.utils._context
193 else:
194 import __builtin__
195 self.context = __builtin__.__dict__
196
197 def parseExpression(self, exp):
198 parsedvar = self.d.expandWithRefs(exp, None)
199 parser = bb.codeparser.PythonParser("ParserTest", logger)
200 parser.parse_python(parsedvar.value)
201
202 self.references = parsedvar.references | parser.references
203 self.execs = parser.execs
204
205 @staticmethod
206 def indent(value):
207 """Python Snippets have to be indented, python values don't have to
208be. These unit tests are testing snippets."""
209 return " " + value
210
211 def test_getvar_reference(self):
212 self.parseExpression("bb.data.getVar('foo', d, True)")
213 self.assertReferences(set(["foo"]))
214 self.assertExecs(set())
215
216 def test_getvar_computed_reference(self):
217 self.parseExpression("bb.data.getVar('f' + 'o' + 'o', d, True)")
218 self.assertReferences(set())
219 self.assertExecs(set())
220
221 def test_getvar_exec_reference(self):
222 self.parseExpression("eval('bb.data.getVar(\"foo\", d, True)')")
223 self.assertReferences(set())
224 self.assertExecs(set(["eval"]))
225
226 def test_var_reference(self):
227 self.context["foo"] = lambda x: x
228 self.setEmptyVars(["FOO"])
229 self.parseExpression("foo('${FOO}')")
230 self.assertReferences(set(["FOO"]))
231 self.assertExecs(set(["foo"]))
232 del self.context["foo"]
233
234 def test_var_exec(self):
235 for etype in ("func", "task"):
236 self.d.setVar("do_something", "echo 'hi mom! ${FOO}'")
237 self.d.setVarFlag("do_something", etype, True)
238 self.parseExpression("bb.build.exec_func('do_something', d)")
239 self.assertReferences(set(["do_something"]))
240
241 def test_function_reference(self):
242 self.context["testfunc"] = lambda msg: bb.msg.note(1, None, msg)
243 self.d.setVar("FOO", "Hello, World!")
244 self.parseExpression("testfunc('${FOO}')")
245 self.assertReferences(set(["FOO"]))
246 self.assertExecs(set(["testfunc"]))
247 del self.context["testfunc"]
248
249 def test_qualified_function_reference(self):
250 self.parseExpression("time.time()")
251 self.assertExecs(set(["time.time"]))
252
253 def test_qualified_function_reference_2(self):
254 self.parseExpression("os.path.dirname('/foo/bar')")
255 self.assertExecs(set(["os.path.dirname"]))
256
257 def test_qualified_function_reference_nested(self):
258 self.parseExpression("time.strftime('%Y%m%d',time.gmtime())")
259 self.assertExecs(set(["time.strftime", "time.gmtime"]))
260
261 def test_function_reference_chained(self):
262 self.context["testget"] = lambda: "\tstrip me "
263 self.parseExpression("testget().strip()")
264 self.assertExecs(set(["testget"]))
265 del self.context["testget"]
266
267
268class DependencyReferenceTest(ReferenceTest):
269
270 pydata = """
271bb.data.getVar('somevar', d, True)
272def test(d):
273 foo = 'bar %s' % 'foo'
274def test2(d):
275 d.getVar(foo, True)
276 d.getVar('bar', False)
277 test2(d)
278
279def a():
280 \"\"\"some
281 stuff
282 \"\"\"
283 return "heh"
284
285test(d)
286
287bb.data.expand(bb.data.getVar("something", False, d), d)
288bb.data.expand("${inexpand} somethingelse", d)
289bb.data.getVar(a(), d, False)
290"""
291
292 def test_python(self):
293 self.d.setVar("FOO", self.pydata)
294 self.setEmptyVars(["inexpand", "a", "test2", "test"])
295 self.d.setVarFlags("FOO", {"func": True, "python": True})
296
297 deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), self.d)
298
299 self.assertEquals(deps, set(["somevar", "bar", "something", "inexpand", "test", "test2", "a"]))
300
301
302 shelldata = """
303foo () {
304bar
305}
306{
307echo baz
308$(heh)
309eval `moo`
310}
311a=b
312c=d
313(
314true && false
315test -f foo
316testval=something
317$testval
318) || aiee
319! inverted
320echo ${somevar}
321
322case foo in
323bar)
324echo bar
325;;
326baz)
327echo baz
328;;
329foo*)
330echo foo
331;;
332esac
333"""
334
335 def test_shell(self):
336 execs = ["bar", "echo", "heh", "moo", "true", "aiee"]
337 self.d.setVar("somevar", "heh")
338 self.d.setVar("inverted", "echo inverted...")
339 self.d.setVarFlag("inverted", "func", True)
340 self.d.setVar("FOO", self.shelldata)
341 self.d.setVarFlags("FOO", {"func": True})
342 self.setEmptyVars(execs)
343
344 deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), self.d)
345
346 self.assertEquals(deps, set(["somevar", "inverted"] + execs))
347
348
349 def test_vardeps(self):
350 self.d.setVar("oe_libinstall", "echo test")
351 self.d.setVar("FOO", "foo=oe_libinstall; eval $foo")
352 self.d.setVarFlag("FOO", "vardeps", "oe_libinstall")
353
354 deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), self.d)
355
356 self.assertEquals(deps, set(["oe_libinstall"]))
357
358 def test_vardeps_expand(self):
359 self.d.setVar("oe_libinstall", "echo test")
360 self.d.setVar("FOO", "foo=oe_libinstall; eval $foo")
361 self.d.setVarFlag("FOO", "vardeps", "${@'oe_libinstall'}")
362
363 deps, values = bb.data.build_dependencies("FOO", set(self.d.keys()), set(), set(), self.d)
364
365 self.assertEquals(deps, set(["oe_libinstall"]))
366
367 #Currently no wildcard support
368 #def test_vardeps_wildcards(self):
369 # self.d.setVar("oe_libinstall", "echo test")
370 # self.d.setVar("FOO", "foo=oe_libinstall; eval $foo")
371 # self.d.setVarFlag("FOO", "vardeps", "oe_*")
372 # self.assertEquals(deps, set(["oe_libinstall"]))
373
374
diff --git a/bitbake/lib/bb/tests/cow.py b/bitbake/lib/bb/tests/cow.py
new file mode 100644
index 0000000000..35c5841f32
--- /dev/null
+++ b/bitbake/lib/bb/tests/cow.py
@@ -0,0 +1,136 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake Tests for Copy-on-Write (cow.py)
5#
6# Copyright 2006 Holger Freyther <freyther@handhelds.org>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22import unittest
23import os
24
25class COWTestCase(unittest.TestCase):
26 """
27 Test case for the COW module from mithro
28 """
29
30 def testGetSet(self):
31 """
32 Test and set
33 """
34 from bb.COW import COWDictBase
35 a = COWDictBase.copy()
36
37 self.assertEquals(False, a.has_key('a'))
38
39 a['a'] = 'a'
40 a['b'] = 'b'
41 self.assertEquals(True, a.has_key('a'))
42 self.assertEquals(True, a.has_key('b'))
43 self.assertEquals('a', a['a'] )
44 self.assertEquals('b', a['b'] )
45
46 def testCopyCopy(self):
47 """
48 Test the copy of copies
49 """
50
51 from bb.COW import COWDictBase
52
53 # create two COW dict 'instances'
54 b = COWDictBase.copy()
55 c = COWDictBase.copy()
56
57 # assign some keys to one instance, some keys to another
58 b['a'] = 10
59 b['c'] = 20
60 c['a'] = 30
61
62 # test separation of the two instances
63 self.assertEquals(False, c.has_key('c'))
64 self.assertEquals(30, c['a'])
65 self.assertEquals(10, b['a'])
66
67 # test copy
68 b_2 = b.copy()
69 c_2 = c.copy()
70
71 self.assertEquals(False, c_2.has_key('c'))
72 self.assertEquals(10, b_2['a'])
73
74 b_2['d'] = 40
75 self.assertEquals(False, c_2.has_key('d'))
76 self.assertEquals(True, b_2.has_key('d'))
77 self.assertEquals(40, b_2['d'])
78 self.assertEquals(False, b.has_key('d'))
79 self.assertEquals(False, c.has_key('d'))
80
81 c_2['d'] = 30
82 self.assertEquals(True, c_2.has_key('d'))
83 self.assertEquals(True, b_2.has_key('d'))
84 self.assertEquals(30, c_2['d'])
85 self.assertEquals(40, b_2['d'])
86 self.assertEquals(False, b.has_key('d'))
87 self.assertEquals(False, c.has_key('d'))
88
89 # test copy of the copy
90 c_3 = c_2.copy()
91 b_3 = b_2.copy()
92 b_3_2 = b_2.copy()
93
94 c_3['e'] = 4711
95 self.assertEquals(4711, c_3['e'])
96 self.assertEquals(False, c_2.has_key('e'))
97 self.assertEquals(False, b_3.has_key('e'))
98 self.assertEquals(False, b_3_2.has_key('e'))
99 self.assertEquals(False, b_2.has_key('e'))
100
101 b_3['e'] = 'viel'
102 self.assertEquals('viel', b_3['e'])
103 self.assertEquals(4711, c_3['e'])
104 self.assertEquals(False, c_2.has_key('e'))
105 self.assertEquals(True, b_3.has_key('e'))
106 self.assertEquals(False, b_3_2.has_key('e'))
107 self.assertEquals(False, b_2.has_key('e'))
108
109 def testCow(self):
110 from bb.COW import COWDictBase
111 c = COWDictBase.copy()
112 c['123'] = 1027
113 c['other'] = 4711
114 c['d'] = { 'abc' : 10, 'bcd' : 20 }
115
116 copy = c.copy()
117
118 self.assertEquals(1027, c['123'])
119 self.assertEquals(4711, c['other'])
120 self.assertEquals({'abc':10, 'bcd':20}, c['d'])
121 self.assertEquals(1027, copy['123'])
122 self.assertEquals(4711, copy['other'])
123 self.assertEquals({'abc':10, 'bcd':20}, copy['d'])
124
125 # cow it now
126 copy['123'] = 1028
127 copy['other'] = 4712
128 copy['d']['abc'] = 20
129
130
131 self.assertEquals(1027, c['123'])
132 self.assertEquals(4711, c['other'])
133 self.assertEquals({'abc':10, 'bcd':20}, c['d'])
134 self.assertEquals(1028, copy['123'])
135 self.assertEquals(4712, copy['other'])
136 self.assertEquals({'abc':20, 'bcd':20}, copy['d'])
diff --git a/bitbake/lib/bb/tests/data.py b/bitbake/lib/bb/tests/data.py
new file mode 100644
index 0000000000..f281a353f4
--- /dev/null
+++ b/bitbake/lib/bb/tests/data.py
@@ -0,0 +1,254 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake Tests for the Data Store (data.py/data_smart.py)
5#
6# Copyright (C) 2010 Chris Larson
7# Copyright (C) 2012 Richard Purdie
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21#
22
23import unittest
24import bb
25import bb.data
26
27class DataExpansions(unittest.TestCase):
28 def setUp(self):
29 self.d = bb.data.init()
30 self.d["foo"] = "value_of_foo"
31 self.d["bar"] = "value_of_bar"
32 self.d["value_of_foo"] = "value_of_'value_of_foo'"
33
34 def test_one_var(self):
35 val = self.d.expand("${foo}")
36 self.assertEqual(str(val), "value_of_foo")
37
38 def test_indirect_one_var(self):
39 val = self.d.expand("${${foo}}")
40 self.assertEqual(str(val), "value_of_'value_of_foo'")
41
42 def test_indirect_and_another(self):
43 val = self.d.expand("${${foo}} ${bar}")
44 self.assertEqual(str(val), "value_of_'value_of_foo' value_of_bar")
45
46 def test_python_snippet(self):
47 val = self.d.expand("${@5*12}")
48 self.assertEqual(str(val), "60")
49
50 def test_expand_in_python_snippet(self):
51 val = self.d.expand("${@'boo ' + '${foo}'}")
52 self.assertEqual(str(val), "boo value_of_foo")
53
54 def test_python_snippet_getvar(self):
55 val = self.d.expand("${@d.getVar('foo', True) + ' ${bar}'}")
56 self.assertEqual(str(val), "value_of_foo value_of_bar")
57
58 def test_python_snippet_syntax_error(self):
59 self.d.setVar("FOO", "${@foo = 5}")
60 self.assertRaises(bb.data_smart.ExpansionError, self.d.getVar, "FOO", True)
61
62 def test_python_snippet_runtime_error(self):
63 self.d.setVar("FOO", "${@int('test')}")
64 self.assertRaises(bb.data_smart.ExpansionError, self.d.getVar, "FOO", True)
65
66 def test_python_snippet_error_path(self):
67 self.d.setVar("FOO", "foo value ${BAR}")
68 self.d.setVar("BAR", "bar value ${@int('test')}")
69 self.assertRaises(bb.data_smart.ExpansionError, self.d.getVar, "FOO", True)
70
71 def test_value_containing_value(self):
72 val = self.d.expand("${@d.getVar('foo', True) + ' ${bar}'}")
73 self.assertEqual(str(val), "value_of_foo value_of_bar")
74
75 def test_reference_undefined_var(self):
76 val = self.d.expand("${undefinedvar} meh")
77 self.assertEqual(str(val), "${undefinedvar} meh")
78
79 def test_double_reference(self):
80 self.d.setVar("BAR", "bar value")
81 self.d.setVar("FOO", "${BAR} foo ${BAR}")
82 val = self.d.getVar("FOO", True)
83 self.assertEqual(str(val), "bar value foo bar value")
84
85 def test_direct_recursion(self):
86 self.d.setVar("FOO", "${FOO}")
87 self.assertRaises(bb.data_smart.ExpansionError, self.d.getVar, "FOO", True)
88
89 def test_indirect_recursion(self):
90 self.d.setVar("FOO", "${BAR}")
91 self.d.setVar("BAR", "${BAZ}")
92 self.d.setVar("BAZ", "${FOO}")
93 self.assertRaises(bb.data_smart.ExpansionError, self.d.getVar, "FOO", True)
94
95 def test_recursion_exception(self):
96 self.d.setVar("FOO", "${BAR}")
97 self.d.setVar("BAR", "${${@'FOO'}}")
98 self.assertRaises(bb.data_smart.ExpansionError, self.d.getVar, "FOO", True)
99
100 def test_incomplete_varexp_single_quotes(self):
101 self.d.setVar("FOO", "sed -i -e 's:IP{:I${:g' $pc")
102 val = self.d.getVar("FOO", True)
103 self.assertEqual(str(val), "sed -i -e 's:IP{:I${:g' $pc")
104
105 def test_nonstring(self):
106 self.d.setVar("TEST", 5)
107 val = self.d.getVar("TEST", True)
108 self.assertEqual(str(val), "5")
109
110 def test_rename(self):
111 self.d.renameVar("foo", "newfoo")
112 self.assertEqual(self.d.getVar("newfoo"), "value_of_foo")
113 self.assertEqual(self.d.getVar("foo"), None)
114
115 def test_deletion(self):
116 self.d.delVar("foo")
117 self.assertEqual(self.d.getVar("foo"), None)
118
119 def test_keys(self):
120 keys = self.d.keys()
121 self.assertEqual(keys, ['value_of_foo', 'foo', 'bar'])
122
123class TestNestedExpansions(unittest.TestCase):
124 def setUp(self):
125 self.d = bb.data.init()
126 self.d["foo"] = "foo"
127 self.d["bar"] = "bar"
128 self.d["value_of_foobar"] = "187"
129
130 def test_refs(self):
131 val = self.d.expand("${value_of_${foo}${bar}}")
132 self.assertEqual(str(val), "187")
133
134 #def test_python_refs(self):
135 # val = self.d.expand("${@${@3}**2 + ${@4}**2}")
136 # self.assertEqual(str(val), "25")
137
138 def test_ref_in_python_ref(self):
139 val = self.d.expand("${@'${foo}' + 'bar'}")
140 self.assertEqual(str(val), "foobar")
141
142 def test_python_ref_in_ref(self):
143 val = self.d.expand("${${@'f'+'o'+'o'}}")
144 self.assertEqual(str(val), "foo")
145
146 def test_deep_nesting(self):
147 depth = 100
148 val = self.d.expand("${" * depth + "foo" + "}" * depth)
149 self.assertEqual(str(val), "foo")
150
151 #def test_deep_python_nesting(self):
152 # depth = 50
153 # val = self.d.expand("${@" * depth + "1" + "+1}" * depth)
154 # self.assertEqual(str(val), str(depth + 1))
155
156 def test_mixed(self):
157 val = self.d.expand("${value_of_${@('${foo}'+'bar')[0:3]}${${@'BAR'.lower()}}}")
158 self.assertEqual(str(val), "187")
159
160 def test_runtime(self):
161 val = self.d.expand("${${@'value_of' + '_f'+'o'+'o'+'b'+'a'+'r'}}")
162 self.assertEqual(str(val), "187")
163
164class TestMemoize(unittest.TestCase):
165 def test_memoized(self):
166 d = bb.data.init()
167 d.setVar("FOO", "bar")
168 self.assertTrue(d.getVar("FOO") is d.getVar("FOO"))
169
170 def test_not_memoized(self):
171 d1 = bb.data.init()
172 d2 = bb.data.init()
173 d1.setVar("FOO", "bar")
174 d2.setVar("FOO", "bar2")
175 self.assertTrue(d1.getVar("FOO") is not d2.getVar("FOO"))
176
177 def test_changed_after_memoized(self):
178 d = bb.data.init()
179 d.setVar("foo", "value of foo")
180 self.assertEqual(str(d.getVar("foo")), "value of foo")
181 d.setVar("foo", "second value of foo")
182 self.assertEqual(str(d.getVar("foo")), "second value of foo")
183
184 def test_same_value(self):
185 d = bb.data.init()
186 d.setVar("foo", "value of")
187 d.setVar("bar", "value of")
188 self.assertEqual(d.getVar("foo"),
189 d.getVar("bar"))
190
191class TestConcat(unittest.TestCase):
192 def setUp(self):
193 self.d = bb.data.init()
194 self.d.setVar("FOO", "foo")
195 self.d.setVar("VAL", "val")
196 self.d.setVar("BAR", "bar")
197
198 def test_prepend(self):
199 self.d.setVar("TEST", "${VAL}")
200 self.d.prependVar("TEST", "${FOO}:")
201 self.assertEqual(self.d.getVar("TEST", True), "foo:val")
202
203 def test_append(self):
204 self.d.setVar("TEST", "${VAL}")
205 self.d.appendVar("TEST", ":${BAR}")
206 self.assertEqual(self.d.getVar("TEST", True), "val:bar")
207
208 def test_multiple_append(self):
209 self.d.setVar("TEST", "${VAL}")
210 self.d.prependVar("TEST", "${FOO}:")
211 self.d.appendVar("TEST", ":val2")
212 self.d.appendVar("TEST", ":${BAR}")
213 self.assertEqual(self.d.getVar("TEST", True), "foo:val:val2:bar")
214
215class TestOverrides(unittest.TestCase):
216 def setUp(self):
217 self.d = bb.data.init()
218 self.d.setVar("OVERRIDES", "foo:bar:local")
219 self.d.setVar("TEST", "testvalue")
220
221 def test_no_override(self):
222 bb.data.update_data(self.d)
223 self.assertEqual(self.d.getVar("TEST", True), "testvalue")
224
225 def test_one_override(self):
226 self.d.setVar("TEST_bar", "testvalue2")
227 bb.data.update_data(self.d)
228 self.assertEqual(self.d.getVar("TEST", True), "testvalue2")
229
230 def test_multiple_override(self):
231 self.d.setVar("TEST_bar", "testvalue2")
232 self.d.setVar("TEST_local", "testvalue3")
233 self.d.setVar("TEST_foo", "testvalue4")
234 bb.data.update_data(self.d)
235 self.assertEqual(self.d.getVar("TEST", True), "testvalue3")
236
237
238class TestFlags(unittest.TestCase):
239 def setUp(self):
240 self.d = bb.data.init()
241 self.d.setVar("foo", "value of foo")
242 self.d.setVarFlag("foo", "flag1", "value of flag1")
243 self.d.setVarFlag("foo", "flag2", "value of flag2")
244
245 def test_setflag(self):
246 self.assertEqual(self.d.getVarFlag("foo", "flag1"), "value of flag1")
247 self.assertEqual(self.d.getVarFlag("foo", "flag2"), "value of flag2")
248
249 def test_delflag(self):
250 self.d.delVarFlag("foo", "flag2")
251 self.assertEqual(self.d.getVarFlag("foo", "flag1"), "value of flag1")
252 self.assertEqual(self.d.getVarFlag("foo", "flag2"), None)
253
254
diff --git a/bitbake/lib/bb/tests/fetch.py b/bitbake/lib/bb/tests/fetch.py
new file mode 100644
index 0000000000..4bcff543fc
--- /dev/null
+++ b/bitbake/lib/bb/tests/fetch.py
@@ -0,0 +1,424 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake Tests for the Fetcher (fetch2/)
5#
6# Copyright (C) 2012 Richard Purdie
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22import unittest
23import tempfile
24import subprocess
25import os
26from bb.fetch2 import URI
27import bb
28
29class URITest(unittest.TestCase):
30 test_uris = {
31 "http://www.google.com/index.html" : {
32 'uri': 'http://www.google.com/index.html',
33 'scheme': 'http',
34 'hostname': 'www.google.com',
35 'port': None,
36 'hostport': 'www.google.com',
37 'path': '/index.html',
38 'userinfo': '',
39 'username': '',
40 'password': '',
41 'params': {},
42 'relative': False
43 },
44 "http://www.google.com/index.html;param1=value1" : {
45 'uri': 'http://www.google.com/index.html;param1=value1',
46 'scheme': 'http',
47 'hostname': 'www.google.com',
48 'port': None,
49 'hostport': 'www.google.com',
50 'path': '/index.html',
51 'userinfo': '',
52 'username': '',
53 'password': '',
54 'params': {
55 'param1': 'value1'
56 },
57 'relative': False
58 },
59 "http://www.example.com:8080/index.html" : {
60 'uri': 'http://www.example.com:8080/index.html',
61 'scheme': 'http',
62 'hostname': 'www.example.com',
63 'port': 8080,
64 'hostport': 'www.example.com:8080',
65 'path': '/index.html',
66 'userinfo': '',
67 'username': '',
68 'password': '',
69 'params': {},
70 'relative': False
71 },
72 "cvs://anoncvs@cvs.handhelds.org/cvs;module=familiar/dist/ipkg" : {
73 'uri': 'cvs://anoncvs@cvs.handhelds.org/cvs;module=familiar/dist/ipkg',
74 'scheme': 'cvs',
75 'hostname': 'cvs.handhelds.org',
76 'port': None,
77 'hostport': 'cvs.handhelds.org',
78 'path': '/cvs',
79 'userinfo': 'anoncvs',
80 'username': 'anoncvs',
81 'password': '',
82 'params': {
83 'module': 'familiar/dist/ipkg'
84 },
85 'relative': False
86 },
87 "cvs://anoncvs:anonymous@cvs.handhelds.org/cvs;tag=V0-99-81;module=familiar/dist/ipkg": {
88 'uri': 'cvs://anoncvs:anonymous@cvs.handhelds.org/cvs;tag=V0-99-81;module=familiar/dist/ipkg',
89 'scheme': 'cvs',
90 'hostname': 'cvs.handhelds.org',
91 'port': None,
92 'hostport': 'cvs.handhelds.org',
93 'path': '/cvs',
94 'userinfo': 'anoncvs:anonymous',
95 'username': 'anoncvs',
96 'password': 'anonymous',
97 'params': {
98 'tag': 'V0-99-81',
99 'module': 'familiar/dist/ipkg'
100 },
101 'relative': False
102 },
103 "file://example.diff": { # NOTE: Not RFC compliant!
104 'uri': 'file:example.diff',
105 'scheme': 'file',
106 'hostname': '',
107 'port': None,
108 'hostport': '',
109 'path': 'example.diff',
110 'userinfo': '',
111 'username': '',
112 'password': '',
113 'params': {},
114 'relative': True
115 },
116 "file:example.diff": { # NOTE: RFC compliant version of the former
117 'uri': 'file:example.diff',
118 'scheme': 'file',
119 'hostname': '',
120 'port': None,
121 'hostport': '',
122 'path': 'example.diff',
123 'userinfo': '',
124 'userinfo': '',
125 'username': '',
126 'password': '',
127 'params': {},
128 'relative': True
129 },
130 "file:///tmp/example.diff": {
131 'uri': 'file:///tmp/example.diff',
132 'scheme': 'file',
133 'hostname': '',
134 'port': None,
135 'hostport': '',
136 'path': '/tmp/example.diff',
137 'userinfo': '',
138 'userinfo': '',
139 'username': '',
140 'password': '',
141 'params': {},
142 'relative': False
143 },
144 "git:///path/example.git": {
145 'uri': 'git:///path/example.git',
146 'scheme': 'git',
147 'hostname': '',
148 'port': None,
149 'hostport': '',
150 'path': '/path/example.git',
151 'userinfo': '',
152 'userinfo': '',
153 'username': '',
154 'password': '',
155 'params': {},
156 'relative': False
157 },
158 "git:path/example.git": {
159 'uri': 'git:path/example.git',
160 'scheme': 'git',
161 'hostname': '',
162 'port': None,
163 'hostport': '',
164 'path': 'path/example.git',
165 'userinfo': '',
166 'userinfo': '',
167 'username': '',
168 'password': '',
169 'params': {},
170 'relative': True
171 },
172 "git://example.net/path/example.git": {
173 'uri': 'git://example.net/path/example.git',
174 'scheme': 'git',
175 'hostname': 'example.net',
176 'port': None,
177 'hostport': 'example.net',
178 'path': '/path/example.git',
179 'userinfo': '',
180 'userinfo': '',
181 'username': '',
182 'password': '',
183 'params': {},
184 'relative': False
185 }
186 }
187
188 def test_uri(self):
189 for test_uri, ref in self.test_uris.items():
190 uri = URI(test_uri)
191
192 self.assertEqual(str(uri), ref['uri'])
193
194 # expected attributes
195 self.assertEqual(uri.scheme, ref['scheme'])
196
197 self.assertEqual(uri.userinfo, ref['userinfo'])
198 self.assertEqual(uri.username, ref['username'])
199 self.assertEqual(uri.password, ref['password'])
200
201 self.assertEqual(uri.hostname, ref['hostname'])
202 self.assertEqual(uri.port, ref['port'])
203 self.assertEqual(uri.hostport, ref['hostport'])
204
205 self.assertEqual(uri.path, ref['path'])
206 self.assertEqual(uri.params, ref['params'])
207
208 self.assertEqual(uri.relative, ref['relative'])
209
210 def test_dict(self):
211 for test in self.test_uris.values():
212 uri = URI()
213
214 self.assertEqual(uri.scheme, '')
215 self.assertEqual(uri.userinfo, '')
216 self.assertEqual(uri.username, '')
217 self.assertEqual(uri.password, '')
218 self.assertEqual(uri.hostname, '')
219 self.assertEqual(uri.port, None)
220 self.assertEqual(uri.path, '')
221 self.assertEqual(uri.params, {})
222
223
224 uri.scheme = test['scheme']
225 self.assertEqual(uri.scheme, test['scheme'])
226
227 uri.userinfo = test['userinfo']
228 self.assertEqual(uri.userinfo, test['userinfo'])
229 self.assertEqual(uri.username, test['username'])
230 self.assertEqual(uri.password, test['password'])
231
232 uri.hostname = test['hostname']
233 self.assertEqual(uri.hostname, test['hostname'])
234 self.assertEqual(uri.hostport, test['hostname'])
235
236 uri.port = test['port']
237 self.assertEqual(uri.port, test['port'])
238 self.assertEqual(uri.hostport, test['hostport'])
239
240 uri.path = test['path']
241 self.assertEqual(uri.path, test['path'])
242
243 uri.params = test['params']
244 self.assertEqual(uri.params, test['params'])
245
246 self.assertEqual(str(uri)+str(uri.relative), str(test['uri'])+str(test['relative']))
247
248 self.assertEqual(str(uri), test['uri'])
249
250 uri.params = {}
251 self.assertEqual(uri.params, {})
252 self.assertEqual(str(uri), (str(uri).split(";"))[0])
253
254class FetcherTest(unittest.TestCase):
255
256 def setUp(self):
257 self.d = bb.data.init()
258 self.tempdir = tempfile.mkdtemp()
259 self.dldir = os.path.join(self.tempdir, "download")
260 os.mkdir(self.dldir)
261 self.d.setVar("DL_DIR", self.dldir)
262 self.unpackdir = os.path.join(self.tempdir, "unpacked")
263 os.mkdir(self.unpackdir)
264 persistdir = os.path.join(self.tempdir, "persistdata")
265 self.d.setVar("PERSISTENT_DIR", persistdir)
266
267 def tearDown(self):
268 bb.utils.prunedir(self.tempdir)
269
270class MirrorUriTest(FetcherTest):
271
272 replaceuris = {
273 ("git://git.invalid.infradead.org/mtd-utils.git;tag=1234567890123456789012345678901234567890", "git://.*/.*", "http://somewhere.org/somedir/")
274 : "http://somewhere.org/somedir/git2_git.invalid.infradead.org.mtd-utils.git.tar.gz",
275 ("git://git.invalid.infradead.org/mtd-utils.git;tag=1234567890123456789012345678901234567890", "git://.*/([^/]+/)*([^/]*)", "git://somewhere.org/somedir/\\2;protocol=http")
276 : "git://somewhere.org/somedir/mtd-utils.git;tag=1234567890123456789012345678901234567890;protocol=http",
277 ("git://git.invalid.infradead.org/foo/mtd-utils.git;tag=1234567890123456789012345678901234567890", "git://.*/([^/]+/)*([^/]*)", "git://somewhere.org/somedir/\\2;protocol=http")
278 : "git://somewhere.org/somedir/mtd-utils.git;tag=1234567890123456789012345678901234567890;protocol=http",
279 ("git://git.invalid.infradead.org/foo/mtd-utils.git;tag=1234567890123456789012345678901234567890", "git://.*/([^/]+/)*([^/]*)", "git://somewhere.org/\\2;protocol=http")
280 : "git://somewhere.org/mtd-utils.git;tag=1234567890123456789012345678901234567890;protocol=http",
281 ("git://someserver.org/bitbake;tag=1234567890123456789012345678901234567890", "git://someserver.org/bitbake", "git://git.openembedded.org/bitbake")
282 : "git://git.openembedded.org/bitbake;tag=1234567890123456789012345678901234567890",
283 ("file://sstate-xyz.tgz", "file://.*", "file:///somewhere/1234/sstate-cache")
284 : "file:///somewhere/1234/sstate-cache/sstate-xyz.tgz",
285 ("file://sstate-xyz.tgz", "file://.*", "file:///somewhere/1234/sstate-cache/")
286 : "file:///somewhere/1234/sstate-cache/sstate-xyz.tgz",
287 ("http://somewhere.org/somedir1/somedir2/somefile_1.2.3.tar.gz", "http://.*/.*", "http://somewhere2.org/somedir3")
288 : "http://somewhere2.org/somedir3/somefile_1.2.3.tar.gz",
289 ("http://somewhere.org/somedir1/somefile_1.2.3.tar.gz", "http://somewhere.org/somedir1/somefile_1.2.3.tar.gz", "http://somewhere2.org/somedir3/somefile_1.2.3.tar.gz")
290 : "http://somewhere2.org/somedir3/somefile_1.2.3.tar.gz",
291 ("http://www.apache.org/dist/subversion/subversion-1.7.1.tar.bz2", "http://www.apache.org/dist", "http://archive.apache.org/dist")
292 : "http://archive.apache.org/dist/subversion/subversion-1.7.1.tar.bz2",
293 ("http://www.apache.org/dist/subversion/subversion-1.7.1.tar.bz2", "http://.*/.*", "file:///somepath/downloads/")
294 : "file:///somepath/downloads/subversion-1.7.1.tar.bz2",
295 ("git://git.invalid.infradead.org/mtd-utils.git;tag=1234567890123456789012345678901234567890", "git://.*/.*", "git://somewhere.org/somedir/BASENAME;protocol=http")
296 : "git://somewhere.org/somedir/mtd-utils.git;tag=1234567890123456789012345678901234567890;protocol=http",
297 ("git://git.invalid.infradead.org/foo/mtd-utils.git;tag=1234567890123456789012345678901234567890", "git://.*/.*", "git://somewhere.org/somedir/BASENAME;protocol=http")
298 : "git://somewhere.org/somedir/mtd-utils.git;tag=1234567890123456789012345678901234567890;protocol=http",
299 ("git://git.invalid.infradead.org/foo/mtd-utils.git;tag=1234567890123456789012345678901234567890", "git://.*/.*", "git://somewhere.org/somedir/MIRRORNAME;protocol=http")
300 : "git://somewhere.org/somedir/git.invalid.infradead.org.foo.mtd-utils.git;tag=1234567890123456789012345678901234567890;protocol=http",
301
302 #Renaming files doesn't work
303 #("http://somewhere.org/somedir1/somefile_1.2.3.tar.gz", "http://somewhere.org/somedir1/somefile_1.2.3.tar.gz", "http://somewhere2.org/somedir3/somefile_2.3.4.tar.gz") : "http://somewhere2.org/somedir3/somefile_2.3.4.tar.gz"
304 #("file://sstate-xyz.tgz", "file://.*/.*", "file:///somewhere/1234/sstate-cache") : "file:///somewhere/1234/sstate-cache/sstate-xyz.tgz",
305 }
306
307 mirrorvar = "http://.*/.* file:///somepath/downloads/ \n" \
308 "git://someserver.org/bitbake git://git.openembedded.org/bitbake \n" \
309 "https://.*/.* file:///someotherpath/downloads/ \n" \
310 "http://.*/.* file:///someotherpath/downloads/ \n"
311
312 def test_urireplace(self):
313 for k, v in self.replaceuris.items():
314 ud = bb.fetch.FetchData(k[0], self.d)
315 ud.setup_localpath(self.d)
316 mirrors = bb.fetch2.mirror_from_string("%s %s" % (k[1], k[2]))
317 newuris, uds = bb.fetch2.build_mirroruris(ud, mirrors, self.d)
318 self.assertEqual([v], newuris)
319
320 def test_urilist1(self):
321 fetcher = bb.fetch.FetchData("http://downloads.yoctoproject.org/releases/bitbake/bitbake-1.0.tar.gz", self.d)
322 mirrors = bb.fetch2.mirror_from_string(self.mirrorvar)
323 uris, uds = bb.fetch2.build_mirroruris(fetcher, mirrors, self.d)
324 self.assertEqual(uris, ['file:///somepath/downloads/bitbake-1.0.tar.gz', 'file:///someotherpath/downloads/bitbake-1.0.tar.gz'])
325
326 def test_urilist2(self):
327 # Catch https:// -> files:// bug
328 fetcher = bb.fetch.FetchData("https://downloads.yoctoproject.org/releases/bitbake/bitbake-1.0.tar.gz", self.d)
329 mirrors = bb.fetch2.mirror_from_string(self.mirrorvar)
330 uris, uds = bb.fetch2.build_mirroruris(fetcher, mirrors, self.d)
331 self.assertEqual(uris, ['file:///someotherpath/downloads/bitbake-1.0.tar.gz'])
332
333class FetcherNetworkTest(FetcherTest):
334
335 if os.environ.get("BB_SKIP_NETTESTS") == "yes":
336 print("Unset BB_SKIP_NETTESTS to run network tests")
337 else:
338 def test_fetch(self):
339 fetcher = bb.fetch.Fetch(["http://downloads.yoctoproject.org/releases/bitbake/bitbake-1.0.tar.gz", "http://downloads.yoctoproject.org/releases/bitbake/bitbake-1.1.tar.gz"], self.d)
340 fetcher.download()
341 self.assertEqual(os.path.getsize(self.dldir + "/bitbake-1.0.tar.gz"), 57749)
342 self.assertEqual(os.path.getsize(self.dldir + "/bitbake-1.1.tar.gz"), 57892)
343 self.d.setVar("BB_NO_NETWORK", "1")
344 fetcher = bb.fetch.Fetch(["http://downloads.yoctoproject.org/releases/bitbake/bitbake-1.0.tar.gz", "http://downloads.yoctoproject.org/releases/bitbake/bitbake-1.1.tar.gz"], self.d)
345 fetcher.download()
346 fetcher.unpack(self.unpackdir)
347 self.assertEqual(len(os.listdir(self.unpackdir + "/bitbake-1.0/")), 9)
348 self.assertEqual(len(os.listdir(self.unpackdir + "/bitbake-1.1/")), 9)
349
350 def test_fetch_mirror(self):
351 self.d.setVar("MIRRORS", "http://.*/.* http://downloads.yoctoproject.org/releases/bitbake")
352 fetcher = bb.fetch.Fetch(["http://invalid.yoctoproject.org/releases/bitbake/bitbake-1.0.tar.gz"], self.d)
353 fetcher.download()
354 self.assertEqual(os.path.getsize(self.dldir + "/bitbake-1.0.tar.gz"), 57749)
355
356 def test_fetch_premirror(self):
357 self.d.setVar("PREMIRRORS", "http://.*/.* http://downloads.yoctoproject.org/releases/bitbake")
358 fetcher = bb.fetch.Fetch(["http://invalid.yoctoproject.org/releases/bitbake/bitbake-1.0.tar.gz"], self.d)
359 fetcher.download()
360 self.assertEqual(os.path.getsize(self.dldir + "/bitbake-1.0.tar.gz"), 57749)
361
362 def gitfetcher(self, url1, url2):
363 def checkrevision(self, fetcher):
364 fetcher.unpack(self.unpackdir)
365 revision = bb.process.run("git rev-parse HEAD", shell=True, cwd=self.unpackdir + "/git")[0].strip()
366 self.assertEqual(revision, "270a05b0b4ba0959fe0624d2a4885d7b70426da5")
367
368 self.d.setVar("BB_GENERATE_MIRROR_TARBALLS", "1")
369 self.d.setVar("SRCREV", "270a05b0b4ba0959fe0624d2a4885d7b70426da5")
370 fetcher = bb.fetch.Fetch([url1], self.d)
371 fetcher.download()
372 checkrevision(self, fetcher)
373 # Wipe out the dldir clone and the unpacked source, turn off the network and check mirror tarball works
374 bb.utils.prunedir(self.dldir + "/git2/")
375 bb.utils.prunedir(self.unpackdir)
376 self.d.setVar("BB_NO_NETWORK", "1")
377 fetcher = bb.fetch.Fetch([url2], self.d)
378 fetcher.download()
379 checkrevision(self, fetcher)
380
381 def test_gitfetch(self):
382 url1 = url2 = "git://git.openembedded.org/bitbake"
383 self.gitfetcher(url1, url2)
384
385 def test_gitfetch_premirror(self):
386 url1 = "git://git.openembedded.org/bitbake"
387 url2 = "git://someserver.org/bitbake"
388 self.d.setVar("PREMIRRORS", "git://someserver.org/bitbake git://git.openembedded.org/bitbake \n")
389 self.gitfetcher(url1, url2)
390
391 def test_gitfetch_premirror2(self):
392 url1 = url2 = "git://someserver.org/bitbake"
393 self.d.setVar("PREMIRRORS", "git://someserver.org/bitbake git://git.openembedded.org/bitbake \n")
394 self.gitfetcher(url1, url2)
395
396 def test_gitfetch_premirror3(self):
397 realurl = "git://git.openembedded.org/bitbake"
398 dummyurl = "git://someserver.org/bitbake"
399 self.sourcedir = self.unpackdir.replace("unpacked", "sourcemirror.git")
400 os.chdir(self.tempdir)
401 bb.process.run("git clone %s %s 2> /dev/null" % (realurl, self.sourcedir), shell=True)
402 self.d.setVar("PREMIRRORS", "%s git://%s;protocol=file \n" % (dummyurl, self.sourcedir))
403 self.gitfetcher(dummyurl, dummyurl)
404
405class URLHandle(unittest.TestCase):
406
407 datatable = {
408 "http://www.google.com/index.html" : ('http', 'www.google.com', '/index.html', '', '', {}),
409 "cvs://anoncvs@cvs.handhelds.org/cvs;module=familiar/dist/ipkg" : ('cvs', 'cvs.handhelds.org', '/cvs', 'anoncvs', '', {'module': 'familiar/dist/ipkg'}),
410 "cvs://anoncvs:anonymous@cvs.handhelds.org/cvs;tag=V0-99-81;module=familiar/dist/ipkg" : ('cvs', 'cvs.handhelds.org', '/cvs', 'anoncvs', 'anonymous', {'tag': 'V0-99-81', 'module': 'familiar/dist/ipkg'})
411 }
412
413 def test_decodeurl(self):
414 for k, v in self.datatable.items():
415 result = bb.fetch.decodeurl(k)
416 self.assertEqual(result, v)
417
418 def test_encodeurl(self):
419 for k, v in self.datatable.items():
420 result = bb.fetch.encodeurl(v)
421 self.assertEqual(result, k)
422
423
424
diff --git a/bitbake/lib/bb/tests/utils.py b/bitbake/lib/bb/tests/utils.py
new file mode 100644
index 0000000000..7c50b1d786
--- /dev/null
+++ b/bitbake/lib/bb/tests/utils.py
@@ -0,0 +1,53 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# BitBake Tests for utils.py
5#
6# Copyright (C) 2012 Richard Purdie
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22import unittest
23import bb
24
25class VerCmpString(unittest.TestCase):
26
27 def test_vercmpstring(self):
28 result = bb.utils.vercmp_string('1', '2')
29 self.assertTrue(result < 0)
30 result = bb.utils.vercmp_string('2', '1')
31 self.assertTrue(result > 0)
32 result = bb.utils.vercmp_string('1', '1.0')
33 self.assertTrue(result < 0)
34 result = bb.utils.vercmp_string('1', '1.1')
35 self.assertTrue(result < 0)
36 result = bb.utils.vercmp_string('1.1', '1_p2')
37 self.assertTrue(result < 0)
38
39 def test_explode_dep_versions(self):
40 correctresult = {"foo" : ["= 1.10"]}
41 result = bb.utils.explode_dep_versions2("foo (= 1.10)")
42 self.assertEqual(result, correctresult)
43 result = bb.utils.explode_dep_versions2("foo (=1.10)")
44 self.assertEqual(result, correctresult)
45 result = bb.utils.explode_dep_versions2("foo ( = 1.10)")
46 self.assertEqual(result, correctresult)
47 result = bb.utils.explode_dep_versions2("foo ( =1.10)")
48 self.assertEqual(result, correctresult)
49 result = bb.utils.explode_dep_versions2("foo ( = 1.10 )")
50 self.assertEqual(result, correctresult)
51 result = bb.utils.explode_dep_versions2("foo ( =1.10 )")
52 self.assertEqual(result, correctresult)
53
diff --git a/bitbake/lib/bb/tinfoil.py b/bitbake/lib/bb/tinfoil.py
new file mode 100644
index 0000000000..751a2d7a23
--- /dev/null
+++ b/bitbake/lib/bb/tinfoil.py
@@ -0,0 +1,96 @@
1# tinfoil: a simple wrapper around cooker for bitbake-based command-line utilities
2#
3# Copyright (C) 2012 Intel Corporation
4# Copyright (C) 2011 Mentor Graphics Corporation
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19import logging
20import warnings
21import os
22import sys
23
24import bb.cache
25import bb.cooker
26import bb.providers
27import bb.utils
28from bb.cooker import state, BBCooker
29from bb.cookerdata import CookerConfiguration, ConfigParameters
30import bb.fetch2
31
32class Tinfoil:
33 def __init__(self, output=sys.stdout):
34 # Needed to avoid deprecation warnings with python 2.6
35 warnings.filterwarnings("ignore", category=DeprecationWarning)
36
37 # Set up logging
38 self.logger = logging.getLogger('BitBake')
39 console = logging.StreamHandler(output)
40 bb.msg.addDefaultlogFilter(console)
41 format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
42 if output.isatty():
43 format.enable_color()
44 console.setFormatter(format)
45 self.logger.addHandler(console)
46
47 self.config = CookerConfiguration()
48 configparams = TinfoilConfigParameters(parse_only=True)
49 self.config.setConfigParameters(configparams)
50 self.config.setServerRegIdleCallback(self.register_idle_function)
51 self.cooker = BBCooker(self.config)
52 self.config_data = self.cooker.data
53 bb.providers.logger.setLevel(logging.ERROR)
54 self.cooker_data = None
55
56 def register_idle_function(self, function, data):
57 pass
58
59 def parseRecipes(self):
60 sys.stderr.write("Parsing recipes..")
61 self.logger.setLevel(logging.WARNING)
62
63 try:
64 while self.cooker.state in (state.initial, state.parsing):
65 self.cooker.updateCache()
66 except KeyboardInterrupt:
67 self.cooker.shutdown()
68 self.cooker.updateCache()
69 sys.exit(2)
70
71 self.logger.setLevel(logging.INFO)
72 sys.stderr.write("done.\n")
73
74 self.cooker_data = self.cooker.recipecache
75
76 def prepare(self, config_only = False):
77 if not self.cooker_data:
78 if config_only:
79 self.cooker.parseConfiguration()
80 self.cooker_data = self.cooker.recipecache
81 else:
82 self.parseRecipes()
83
84class TinfoilConfigParameters(ConfigParameters):
85
86 def __init__(self, **options):
87 self.initial_options = options
88 super(TinfoilConfigParameters, self).__init__()
89
90 def parseCommandLine(self):
91 class DummyOptions:
92 def __init__(self, initial_options):
93 for key, val in initial_options.items():
94 setattr(self, key, val)
95
96 return DummyOptions(self.initial_options), None
diff --git a/bitbake/lib/bb/ui/__init__.py b/bitbake/lib/bb/ui/__init__.py
new file mode 100644
index 0000000000..a4805ed028
--- /dev/null
+++ b/bitbake/lib/bb/ui/__init__.py
@@ -0,0 +1,17 @@
1#
2# BitBake UI Implementation
3#
4# Copyright (C) 2006-2007 Richard Purdie
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
diff --git a/bitbake/lib/bb/ui/crumbs/__init__.py b/bitbake/lib/bb/ui/crumbs/__init__.py
new file mode 100644
index 0000000000..b7cbe1a4f3
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/__init__.py
@@ -0,0 +1,17 @@
1#
2# Gtk+ UI pieces for BitBake
3#
4# Copyright (C) 2006-2007 Richard Purdie
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
diff --git a/bitbake/lib/bb/ui/crumbs/builddetailspage.py b/bitbake/lib/bb/ui/crumbs/builddetailspage.py
new file mode 100755
index 0000000000..171a7a68ed
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/builddetailspage.py
@@ -0,0 +1,436 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2012 Intel Corporation
6#
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import pango
25import gobject
26import bb.process
27from bb.ui.crumbs.progressbar import HobProgressBar
28from bb.ui.crumbs.hobwidget import hic, HobNotebook, HobAltButton, HobWarpCellRendererText, HobButton, HobInfoButton
29from bb.ui.crumbs.runningbuild import RunningBuildTreeView
30from bb.ui.crumbs.runningbuild import BuildFailureTreeView
31from bb.ui.crumbs.hobpages import HobPage
32from bb.ui.crumbs.hobcolor import HobColors
33
34class BuildConfigurationTreeView(gtk.TreeView):
35 def __init__ (self):
36 gtk.TreeView.__init__(self)
37 self.set_rules_hint(False)
38 self.set_headers_visible(False)
39 self.set_property("hover-expand", True)
40 self.get_selection().set_mode(gtk.SELECTION_SINGLE)
41
42 # The icon that indicates whether we're building or failed.
43 renderer0 = gtk.CellRendererText()
44 renderer0.set_property('font-desc', pango.FontDescription('courier bold 12'))
45 col0 = gtk.TreeViewColumn ("Name", renderer0, text=0)
46 self.append_column (col0)
47
48 # The message of configuration.
49 renderer1 = HobWarpCellRendererText(col_number=1)
50 col1 = gtk.TreeViewColumn ("Values", renderer1, text=1)
51 self.append_column (col1)
52
53 def set_vars(self, key="", var=[""]):
54 d = {}
55 if type(var) == str:
56 d = {key: [var]}
57 elif type(var) == list and len(var) > 1:
58 #create the sub item line
59 l = []
60 text = ""
61 for item in var:
62 text = " - " + item
63 l.append(text)
64 d = {key: var}
65
66 return d
67
68 def set_config_model(self, show_vars):
69 listmodel = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
70 parent = None
71 for var in show_vars:
72 for subitem in var.items():
73 name = subitem[0]
74 is_parent = True
75 for value in subitem[1]:
76 if is_parent:
77 parent = listmodel.append(parent, (name, value))
78 is_parent = False
79 else:
80 listmodel.append(parent, (None, value))
81 name = " - "
82 parent = None
83 # renew the tree model after get the configuration messages
84 self.set_model(listmodel)
85
86 def show(self, src_config_info, src_params):
87 vars = []
88 vars.append(self.set_vars("BB version:", src_params.bb_version))
89 vars.append(self.set_vars("Target arch:", src_params.target_arch))
90 vars.append(self.set_vars("Target OS:", src_params.target_os))
91 vars.append(self.set_vars("Machine:", src_config_info.curr_mach))
92 vars.append(self.set_vars("Distro:", src_config_info.curr_distro))
93 vars.append(self.set_vars("Distro version:", src_params.distro_version))
94 vars.append(self.set_vars("SDK machine:", src_config_info.curr_sdk_machine))
95 vars.append(self.set_vars("Tune features:", src_params.tune_pkgarch))
96 vars.append(self.set_vars("Layers:", src_config_info.layers))
97
98 for path in src_config_info.layers:
99 import os, os.path
100 if os.path.exists(path):
101 branch = bb.process.run('cd %s; git branch | grep "^* " | tr -d "* "' % path)[0]
102 if branch.startswith("fatal:"):
103 branch = "(unknown)"
104 if branch:
105 branch = branch.strip('\n')
106 vars.append(self.set_vars("Branch:", branch))
107 break
108
109 self.set_config_model(vars)
110
111 def reset(self):
112 self.set_model(None)
113
114#
115# BuildDetailsPage
116#
117
118class BuildDetailsPage (HobPage):
119
120 def __init__(self, builder):
121 super(BuildDetailsPage, self).__init__(builder, "Building ...")
122
123 self.num_of_issues = 0
124 self.endpath = (0,)
125 # create visual elements
126 self.create_visual_elements()
127
128 def create_visual_elements(self):
129 # create visual elements
130 self.vbox = gtk.VBox(False, 12)
131
132 self.progress_box = gtk.VBox(False, 12)
133 self.task_status = gtk.Label("\n") # to ensure layout is correct
134 self.task_status.set_alignment(0.0, 0.5)
135 self.progress_box.pack_start(self.task_status, expand=False, fill=False)
136 self.progress_hbox = gtk.HBox(False, 6)
137 self.progress_box.pack_end(self.progress_hbox, expand=True, fill=True)
138 self.progress_bar = HobProgressBar()
139 self.progress_hbox.pack_start(self.progress_bar, expand=True, fill=True)
140 self.stop_button = HobAltButton("Stop")
141 self.stop_button.connect("clicked", self.stop_button_clicked_cb)
142 self.stop_button.set_sensitive(False)
143 self.progress_hbox.pack_end(self.stop_button, expand=False, fill=False)
144
145 self.notebook = HobNotebook()
146 self.config_tv = BuildConfigurationTreeView()
147 self.scrolled_view_config = gtk.ScrolledWindow ()
148 self.scrolled_view_config.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
149 self.scrolled_view_config.add(self.config_tv)
150 self.notebook.append_page(self.scrolled_view_config, "Build configuration")
151
152 self.failure_tv = BuildFailureTreeView()
153 self.failure_model = self.builder.handler.build.model.failure_model()
154 self.failure_tv.set_model(self.failure_model)
155 self.scrolled_view_failure = gtk.ScrolledWindow ()
156 self.scrolled_view_failure.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
157 self.scrolled_view_failure.add(self.failure_tv)
158 self.notebook.append_page(self.scrolled_view_failure, "Issues")
159
160 self.build_tv = RunningBuildTreeView(readonly=True, hob=True)
161 self.build_tv.set_model(self.builder.handler.build.model)
162 self.scrolled_view_build = gtk.ScrolledWindow ()
163 self.scrolled_view_build.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
164 self.scrolled_view_build.add(self.build_tv)
165 self.notebook.append_page(self.scrolled_view_build, "Log")
166
167 self.builder.handler.build.model.connect_after("row-changed", self.scroll_to_present_row, self.scrolled_view_build.get_vadjustment(), self.build_tv)
168
169 self.button_box = gtk.HBox(False, 6)
170 self.back_button = HobAltButton('&lt;&lt; Back')
171 self.back_button.connect("clicked", self.back_button_clicked_cb)
172 self.button_box.pack_start(self.back_button, expand=False, fill=False)
173
174 def update_build_status(self, current, total, task):
175 recipe_path, recipe_task = task.split(", ")
176 recipe = os.path.basename(recipe_path).rstrip(".bb")
177 tsk_msg = "<b>Running task %s of %s:</b> %s\n<b>Recipe:</b> %s" % (current, total, recipe_task, recipe)
178 self.task_status.set_markup(tsk_msg)
179 self.stop_button.set_sensitive(True)
180
181 def reset_build_status(self):
182 self.task_status.set_markup("\n") # to ensure layout is correct
183 self.endpath = (0,)
184
185 def show_issues(self):
186 self.num_of_issues += 1
187 self.notebook.show_indicator_icon("Issues", self.num_of_issues)
188
189 def reset_issues(self):
190 self.num_of_issues = 0
191 self.notebook.hide_indicator_icon("Issues")
192
193 def _remove_all_widget(self):
194 children = self.vbox.get_children() or []
195 for child in children:
196 self.vbox.remove(child)
197 children = self.box_group_area.get_children() or []
198 for child in children:
199 self.box_group_area.remove(child)
200 children = self.get_children() or []
201 for child in children:
202 self.remove(child)
203
204 def add_build_fail_top_bar(self, actions, log_file=None):
205 primary_action = "Edit %s" % actions
206
207 color = HobColors.ERROR
208 build_fail_top = gtk.EventBox()
209 #build_fail_top.set_size_request(-1, 200)
210 build_fail_top.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
211
212 build_fail_tab = gtk.Table(14, 46, True)
213 build_fail_top.add(build_fail_tab)
214
215 icon = gtk.Image()
216 icon_pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_INDI_ERROR_FILE)
217 icon.set_from_pixbuf(icon_pix_buffer)
218 build_fail_tab.attach(icon, 1, 4, 0, 6)
219
220 label = gtk.Label()
221 label.set_alignment(0.0, 0.5)
222 label.set_markup("<span size='x-large'><b>%s</b></span>" % self.title)
223 build_fail_tab.attach(label, 4, 26, 0, 6)
224
225 label = gtk.Label()
226 label.set_alignment(0.0, 0.5)
227 # Ensure variable disk_full is defined
228 if not hasattr(self.builder, 'disk_full'):
229 self.builder.disk_full = False
230
231 if self.builder.disk_full:
232 markup = "<span size='medium'>There is no disk space left, so Hob cannot finish building your image. Free up some disk space\n"
233 markup += "and restart the build. Check the \"Issues\" tab for more details</span>"
234 label.set_markup(markup)
235 else:
236 label.set_markup("<span size='medium'>Check the \"Issues\" information for more details</span>")
237 build_fail_tab.attach(label, 4, 40, 4, 9)
238
239 # create button 'Edit packages'
240 action_button = HobButton(primary_action)
241 #action_button.set_size_request(-1, 40)
242 action_button.set_tooltip_text("Edit the %s parameters" % actions)
243 action_button.connect('clicked', self.failure_primary_action_button_clicked_cb, primary_action)
244
245 if log_file:
246 open_log_button = HobAltButton("Open log")
247 open_log_button.set_relief(gtk.RELIEF_HALF)
248 open_log_button.set_tooltip_text("Open the build's log file")
249 open_log_button.connect('clicked', self.open_log_button_clicked_cb, log_file)
250
251 attach_pos = (24 if log_file else 14)
252 file_bug_button = HobAltButton('File a bug')
253 file_bug_button.set_relief(gtk.RELIEF_HALF)
254 file_bug_button.set_tooltip_text("Open the Yocto Project bug tracking website")
255 file_bug_button.connect('clicked', self.failure_activate_file_bug_link_cb)
256
257 if not self.builder.disk_full:
258 build_fail_tab.attach(action_button, 4, 13, 9, 12)
259 if log_file:
260 build_fail_tab.attach(open_log_button, 14, 23, 9, 12)
261 build_fail_tab.attach(file_bug_button, attach_pos, attach_pos + 9, 9, 12)
262
263 else:
264 restart_build = HobButton("Restart the build")
265 restart_build.set_tooltip_text("Restart the build")
266 restart_build.connect('clicked', self.restart_build_button_clicked_cb)
267
268 build_fail_tab.attach(restart_build, 4, 13, 9, 12)
269 build_fail_tab.attach(action_button, 14, 23, 9, 12)
270 if log_file:
271 build_fail_tab.attach(open_log_button, attach_pos, attach_pos + 9, 9, 12)
272
273 self.builder.disk_full = False
274 return build_fail_top
275
276 def show_fail_page(self, title):
277 self._remove_all_widget()
278 self.title = "Hob cannot build your %s" % title
279
280 self.build_fail_bar = self.add_build_fail_top_bar(title, self.builder.current_logfile)
281
282 self.pack_start(self.group_align, expand=True, fill=True)
283 self.box_group_area.pack_start(self.build_fail_bar, expand=False, fill=False)
284 self.box_group_area.pack_start(self.vbox, expand=True, fill=True)
285
286 self.vbox.pack_start(self.notebook, expand=True, fill=True)
287 self.show_all()
288 self.notebook.set_page("Issues")
289 self.back_button.hide()
290
291 def add_build_stop_top_bar(self, action, log_file=None):
292 color = HobColors.LIGHT_GRAY
293 build_stop_top = gtk.EventBox()
294 #build_stop_top.set_size_request(-1, 200)
295 build_stop_top.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
296 build_stop_top.set_flags(gtk.CAN_DEFAULT)
297 build_stop_top.grab_default()
298
299 build_stop_tab = gtk.Table(11, 46, True)
300 build_stop_top.add(build_stop_tab)
301
302 icon = gtk.Image()
303 icon_pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_INFO_HOVER_FILE)
304 icon.set_from_pixbuf(icon_pix_buffer)
305 build_stop_tab.attach(icon, 1, 4, 0, 6)
306
307 label = gtk.Label()
308 label.set_alignment(0.0, 0.5)
309 label.set_markup("<span size='x-large'><b>%s</b></span>" % self.title)
310 build_stop_tab.attach(label, 4, 26, 0, 6)
311
312 action_button = HobButton("Edit %s" % action)
313 action_button.set_size_request(-1, 40)
314 if action == "image":
315 action_button.set_tooltip_text("Edit the image parameters")
316 elif action == "recipes":
317 action_button.set_tooltip_text("Edit the included recipes")
318 elif action == "packages":
319 action_button.set_tooltip_text("Edit the included packages")
320 action_button.connect('clicked', self.stop_primary_action_button_clicked_cb, action)
321 build_stop_tab.attach(action_button, 4, 13, 6, 9)
322
323 if log_file:
324 open_log_button = HobAltButton("Open log")
325 open_log_button.set_relief(gtk.RELIEF_HALF)
326 open_log_button.set_tooltip_text("Open the build's log file")
327 open_log_button.connect('clicked', self.open_log_button_clicked_cb, log_file)
328 build_stop_tab.attach(open_log_button, 14, 23, 6, 9)
329
330 attach_pos = (24 if log_file else 14)
331 build_button = HobAltButton("Build new image")
332 #build_button.set_size_request(-1, 40)
333 build_button.set_tooltip_text("Create a new image from scratch")
334 build_button.connect('clicked', self.new_image_button_clicked_cb)
335 build_stop_tab.attach(build_button, attach_pos, attach_pos + 9, 6, 9)
336
337 return build_stop_top, action_button
338
339 def show_stop_page(self, action):
340 self._remove_all_widget()
341 self.title = "Build stopped"
342 self.build_stop_bar, action_button = self.add_build_stop_top_bar(action, self.builder.current_logfile)
343
344 self.pack_start(self.group_align, expand=True, fill=True)
345 self.box_group_area.pack_start(self.build_stop_bar, expand=False, fill=False)
346 self.box_group_area.pack_start(self.vbox, expand=True, fill=True)
347
348 self.vbox.pack_start(self.notebook, expand=True, fill=True)
349 self.show_all()
350 self.back_button.hide()
351 return action_button
352
353 def show_page(self, step):
354 self._remove_all_widget()
355 if step == self.builder.PACKAGE_GENERATING or step == self.builder.FAST_IMAGE_GENERATING:
356 self.title = "Building packages ..."
357 else:
358 self.title = "Building image ..."
359 self.build_details_top = self.add_onto_top_bar(None)
360 self.pack_start(self.build_details_top, expand=False, fill=False)
361 self.pack_start(self.group_align, expand=True, fill=True)
362
363 self.box_group_area.pack_start(self.vbox, expand=True, fill=True)
364
365 self.progress_bar.reset()
366 self.config_tv.reset()
367 self.vbox.pack_start(self.progress_box, expand=False, fill=False)
368
369 self.vbox.pack_start(self.notebook, expand=True, fill=True)
370
371 self.box_group_area.pack_end(self.button_box, expand=False, fill=False)
372 self.show_all()
373 self.notebook.set_page("Log")
374 self.back_button.hide()
375
376 self.reset_build_status()
377 self.reset_issues()
378
379 def update_progress_bar(self, title, fraction, status=None):
380 self.progress_bar.update(fraction)
381 self.progress_bar.set_title(title)
382 self.progress_bar.set_rcstyle(status)
383
384 def back_button_clicked_cb(self, button):
385 self.builder.show_configuration()
386
387 def new_image_button_clicked_cb(self, button):
388 self.builder.reset()
389
390 def show_back_button(self):
391 self.back_button.show()
392
393 def stop_button_clicked_cb(self, button):
394 self.builder.stop_build()
395
396 def hide_stop_button(self):
397 self.stop_button.set_sensitive(False)
398 self.stop_button.hide()
399
400 def scroll_to_present_row(self, model, path, iter, v_adj, treeview):
401 if treeview and v_adj:
402 if path[0] > self.endpath[0]: # check the event is a new row append or not
403 self.endpath = path
404 # check the gtk.adjustment position is at end boundary or not
405 if (v_adj.upper <= v_adj.page_size) or (v_adj.value == v_adj.upper - v_adj.page_size):
406 treeview.scroll_to_cell(path)
407
408 def show_configurations(self, configurations, params):
409 self.config_tv.show(configurations, params)
410
411 def failure_primary_action_button_clicked_cb(self, button, action):
412 if "Edit recipes" in action:
413 self.builder.show_recipes()
414 elif "Edit packages" in action:
415 self.builder.show_packages()
416 elif "Edit image" in action:
417 self.builder.show_configuration()
418
419 def restart_build_button_clicked_cb(self, button):
420 self.builder.just_bake()
421
422 def stop_primary_action_button_clicked_cb(self, button, action):
423 if "recipes" in action:
424 self.builder.show_recipes()
425 elif "packages" in action:
426 self.builder.show_packages(ask=False)
427 elif "image" in action:
428 self.builder.show_configuration()
429
430 def open_log_button_clicked_cb(self, button, log_file):
431 if log_file:
432 log_file = "file:///" + log_file
433 gtk.show_uri(screen=button.get_screen(), uri=log_file, timestamp=0)
434
435 def failure_activate_file_bug_link_cb(self, button):
436 button.child.emit('activate-link', "http://bugzilla.yoctoproject.org")
diff --git a/bitbake/lib/bb/ui/crumbs/builder.py b/bitbake/lib/bb/ui/crumbs/builder.py
new file mode 100755
index 0000000000..ab6750b741
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/builder.py
@@ -0,0 +1,1476 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2011-2012 Intel Corporation
6#
7# Authored by Joshua Lock <josh@linux.intel.com>
8# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
9# Authored by Shane Wang <shane.wang@intel.com>
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License version 2 as
13# published by the Free Software Foundation.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License along
21# with this program; if not, write to the Free Software Foundation, Inc.,
22# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
24import glib
25import gtk, gobject
26import copy
27import os
28import subprocess
29import shlex
30import re
31import logging
32import sys
33import signal
34import time
35from bb.ui.crumbs.imageconfigurationpage import ImageConfigurationPage
36from bb.ui.crumbs.recipeselectionpage import RecipeSelectionPage
37from bb.ui.crumbs.packageselectionpage import PackageSelectionPage
38from bb.ui.crumbs.builddetailspage import BuildDetailsPage
39from bb.ui.crumbs.imagedetailspage import ImageDetailsPage
40from bb.ui.crumbs.sanitycheckpage import SanityCheckPage
41from bb.ui.crumbs.hobwidget import hwc, HobButton, HobAltButton
42from bb.ui.crumbs.persistenttooltip import PersistentTooltip
43import bb.ui.crumbs.utils
44from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog
45from bb.ui.crumbs.hig.simplesettingsdialog import SimpleSettingsDialog
46from bb.ui.crumbs.hig.advancedsettingsdialog import AdvancedSettingsDialog
47from bb.ui.crumbs.hig.deployimagedialog import DeployImageDialog
48from bb.ui.crumbs.hig.layerselectiondialog import LayerSelectionDialog
49from bb.ui.crumbs.hig.imageselectiondialog import ImageSelectionDialog
50from bb.ui.crumbs.hig.parsingwarningsdialog import ParsingWarningsDialog
51from bb.ui.crumbs.hig.propertydialog import PropertyDialog
52
53hobVer = 20120808
54
55class Configuration:
56 '''Represents the data structure of configuration.'''
57
58 @classmethod
59 def parse_proxy_string(cls, proxy):
60 pattern = "^\s*((http|https|ftp|socks|cvs)://)?((\S+):(\S+)@)?([^\s:]+)(:(\d+))?/?"
61 match = re.search(pattern, proxy)
62 if match:
63 return match.group(2), match.group(4), match.group(5), match.group(6), match.group(8)
64 else:
65 return None, None, None, "", ""
66
67 @classmethod
68 def make_host_string(cls, prot, user, passwd, host, default_prot=""):
69 if host == None or host == "":
70 return ""
71
72 passwd = passwd or ""
73
74 if user != None and user != "":
75 if prot == None or prot == "":
76 prot = default_prot
77 return prot + "://" + user + ":" + passwd + "@" + host
78 else:
79 if prot == None or prot == "":
80 return host
81 else:
82 return prot + "://" + host
83
84 @classmethod
85 def make_port_string(cls, port):
86 port = port or ""
87 return port
88
89 @classmethod
90 def make_proxy_string(cls, prot, user, passwd, host, port, default_prot=""):
91 if host == None or host == "":# or port == None or port == "":
92 return ""
93
94 return Configuration.make_host_string(prot, user, passwd, host, default_prot) + (":" + Configuration.make_port_string(port) if port else "")
95
96 def __init__(self):
97 self.curr_mach = ""
98 self.selected_image = None
99 # settings
100 self.curr_distro = ""
101 self.dldir = self.sstatedir = self.sstatemirror = ""
102 self.pmake = self.bbthread = 0
103 self.curr_package_format = ""
104 self.image_rootfs_size = self.image_extra_size = 0
105 self.image_overhead_factor = 1
106 self.incompat_license = ""
107 self.curr_sdk_machine = ""
108 self.conf_version = self.lconf_version = ""
109 self.extra_setting = {}
110 self.toolchain_build = False
111 self.image_fstypes = ""
112 self.image_size = None
113 self.image_packages = []
114 # bblayers.conf
115 self.layers = []
116 # image/recipes/packages
117 self.clear_selection()
118
119 self.user_selected_packages = []
120
121 self.default_task = "build"
122
123 # proxy settings
124 self.enable_proxy = None
125 self.same_proxy = False
126 self.proxies = {
127 "http" : [None, None, None, "", ""], # protocol : [prot, user, passwd, host, port]
128 "https" : [None, None, None, "", ""],
129 "ftp" : [None, None, None, "", ""],
130 "socks" : [None, None, None, "", ""],
131 "cvs" : [None, None, None, "", ""],
132 }
133
134 def clear_selection(self):
135 self.selected_recipes = []
136 self.selected_packages = []
137 self.initial_selected_image = None
138 self.initial_selected_packages = []
139 self.initial_user_selected_packages = []
140
141 def split_proxy(self, protocol, proxy):
142 entry = []
143 prot, user, passwd, host, port = Configuration.parse_proxy_string(proxy)
144 entry.append(prot)
145 entry.append(user)
146 entry.append(passwd)
147 entry.append(host)
148 entry.append(port)
149 self.proxies[protocol] = entry
150
151 def combine_proxy(self, protocol):
152 entry = self.proxies[protocol]
153 return Configuration.make_proxy_string(entry[0], entry[1], entry[2], entry[3], entry[4], protocol)
154
155 def combine_host_only(self, protocol):
156 entry = self.proxies[protocol]
157 return Configuration.make_host_string(entry[0], entry[1], entry[2], entry[3], protocol)
158
159 def combine_port_only(self, protocol):
160 entry = self.proxies[protocol]
161 return Configuration.make_port_string(entry[4])
162
163 def update(self, params):
164 # settings
165 self.curr_distro = params["distro"]
166 self.dldir = params["dldir"]
167 self.sstatedir = params["sstatedir"]
168 self.sstatemirror = params["sstatemirror"]
169 self.pmake = int(params["pmake"].split()[1])
170 self.bbthread = params["bbthread"]
171 self.curr_package_format = " ".join(params["pclass"].split("package_")).strip()
172 self.image_rootfs_size = params["image_rootfs_size"]
173 self.image_extra_size = params["image_extra_size"]
174 self.image_overhead_factor = params['image_overhead_factor']
175 self.incompat_license = params["incompat_license"]
176 self.curr_sdk_machine = params["sdk_machine"]
177 self.conf_version = params["conf_version"]
178 self.lconf_version = params["lconf_version"]
179 self.image_fstypes = params["image_fstypes"]
180 # self.extra_setting/self.toolchain_build
181 # bblayers.conf
182 self.layers = params["layer"].split()
183 self.layers_non_removable = params["layers_non_removable"].split()
184 self.default_task = params["default_task"]
185
186 # proxy settings
187 self.enable_proxy = params["http_proxy"] != "" or params["https_proxy"] != "" \
188 or params["ftp_proxy"] != "" or params["socks_proxy"] != "" \
189 or params["cvs_proxy_host"] != "" or params["cvs_proxy_port"] != ""
190 self.split_proxy("http", params["http_proxy"])
191 self.split_proxy("https", params["https_proxy"])
192 self.split_proxy("ftp", params["ftp_proxy"])
193 self.split_proxy("socks", params["socks_proxy"])
194 self.split_proxy("cvs", params["cvs_proxy_host"] + ":" + params["cvs_proxy_port"])
195
196 def save(self, handler, defaults=False):
197 # bblayers.conf
198 handler.set_var_in_file("BBLAYERS", self.layers, "bblayers.conf")
199 # local.conf
200 if not defaults:
201 handler.early_assign_var_in_file("MACHINE", self.curr_mach, "local.conf")
202 handler.set_var_in_file("DISTRO", self.curr_distro, "local.conf")
203 handler.set_var_in_file("DL_DIR", self.dldir, "local.conf")
204 handler.set_var_in_file("SSTATE_DIR", self.sstatedir, "local.conf")
205 sstate_mirror_list = self.sstatemirror.split("\\n ")
206 sstate_mirror_list_modified = []
207 for mirror in sstate_mirror_list:
208 if mirror != "":
209 mirror = mirror + "\\n"
210 sstate_mirror_list_modified.append(mirror)
211 handler.set_var_in_file("SSTATE_MIRRORS", sstate_mirror_list_modified, "local.conf")
212 handler.set_var_in_file("PARALLEL_MAKE", "-j %s" % self.pmake, "local.conf")
213 handler.set_var_in_file("BB_NUMBER_THREADS", self.bbthread, "local.conf")
214 handler.set_var_in_file("PACKAGE_CLASSES", " ".join(["package_" + i for i in self.curr_package_format.split()]), "local.conf")
215 handler.set_var_in_file("IMAGE_ROOTFS_SIZE", self.image_rootfs_size, "local.conf")
216 handler.set_var_in_file("IMAGE_EXTRA_SPACE", self.image_extra_size, "local.conf")
217 handler.set_var_in_file("INCOMPATIBLE_LICENSE", self.incompat_license, "local.conf")
218 handler.set_var_in_file("SDKMACHINE", self.curr_sdk_machine, "local.conf")
219 handler.set_var_in_file("CONF_VERSION", self.conf_version, "local.conf")
220 handler.set_var_in_file("LCONF_VERSION", self.lconf_version, "bblayers.conf")
221 handler.set_extra_config(self.extra_setting)
222 handler.set_var_in_file("TOOLCHAIN_BUILD", self.toolchain_build, "local.conf")
223 handler.set_var_in_file("IMAGE_FSTYPES", self.image_fstypes, "local.conf")
224 if not defaults:
225 # image/recipes/packages
226 handler.set_var_in_file("__SELECTED_IMAGE__", self.selected_image, "local.conf")
227 handler.set_var_in_file("DEPENDS", self.selected_recipes, "local.conf")
228 handler.set_var_in_file("IMAGE_INSTALL", self.user_selected_packages, "local.conf")
229 # proxy
230 handler.set_var_in_file("enable_proxy", self.enable_proxy, "local.conf")
231 handler.set_var_in_file("use_same_proxy", self.same_proxy, "local.conf")
232 handler.set_var_in_file("http_proxy", self.combine_proxy("http"), "local.conf")
233 handler.set_var_in_file("https_proxy", self.combine_proxy("https"), "local.conf")
234 handler.set_var_in_file("ftp_proxy", self.combine_proxy("ftp"), "local.conf")
235 handler.set_var_in_file("all_proxy", self.combine_proxy("socks"), "local.conf")
236 handler.set_var_in_file("CVS_PROXY_HOST", self.combine_host_only("cvs"), "local.conf")
237 handler.set_var_in_file("CVS_PROXY_PORT", self.combine_port_only("cvs"), "local.conf")
238
239 def __str__(self):
240 s = "VERSION: '%s', BBLAYERS: '%s', MACHINE: '%s', DISTRO: '%s', DL_DIR: '%s'," % \
241 (hobVer, " ".join(self.layers), self.curr_mach, self.curr_distro, self.dldir )
242 s += "SSTATE_DIR: '%s', SSTATE_MIRROR: '%s', PARALLEL_MAKE: '-j %s', BB_NUMBER_THREADS: '%s', PACKAGE_CLASSES: '%s', " % \
243 (self.sstatedir, self.sstatemirror, self.pmake, self.bbthread, " ".join(["package_" + i for i in self.curr_package_format.split()]))
244 s += "IMAGE_ROOTFS_SIZE: '%s', IMAGE_EXTRA_SPACE: '%s', INCOMPATIBLE_LICENSE: '%s', SDKMACHINE: '%s', CONF_VERSION: '%s', " % \
245 (self.image_rootfs_size, self.image_extra_size, self.incompat_license, self.curr_sdk_machine, self.conf_version)
246 s += "LCONF_VERSION: '%s', EXTRA_SETTING: '%s', TOOLCHAIN_BUILD: '%s', IMAGE_FSTYPES: '%s', __SELECTED_IMAGE__: '%s', " % \
247 (self.lconf_version, self.extra_setting, self.toolchain_build, self.image_fstypes, self.selected_image)
248 s += "DEPENDS: '%s', IMAGE_INSTALL: '%s', enable_proxy: '%s', use_same_proxy: '%s', http_proxy: '%s', " % \
249 (self.selected_recipes, self.user_selected_packages, self.enable_proxy, self.same_proxy, self.combine_proxy("http"))
250 s += "https_proxy: '%s', ftp_proxy: '%s', all_proxy: '%s', CVS_PROXY_HOST: '%s', CVS_PROXY_PORT: '%s'" % \
251 (self.combine_proxy("https"), self.combine_proxy("ftp"), self.combine_proxy("socks"),
252 self.combine_host_only("cvs"), self.combine_port_only("cvs"))
253 return s
254
255class Parameters:
256 '''Represents other variables like available machines, etc.'''
257
258 def __init__(self):
259 # Variables
260 self.max_threads = 65535
261 self.core_base = ""
262 self.image_addr = ""
263 self.image_types = []
264 self.runnable_image_types = []
265 self.runnable_machine_patterns = []
266 self.deployable_image_types = []
267 self.tmpdir = ""
268
269 self.all_machines = []
270 self.all_package_formats = []
271 self.all_distros = []
272 self.all_sdk_machines = []
273 self.all_layers = []
274 self.image_names = []
275 self.image_white_pattern = ""
276 self.image_black_pattern = ""
277
278 # for build log to show
279 self.bb_version = ""
280 self.target_arch = ""
281 self.target_os = ""
282 self.distro_version = ""
283 self.tune_pkgarch = ""
284
285 def update(self, params):
286 self.max_threads = params["max_threads"]
287 self.core_base = params["core_base"]
288 self.image_addr = params["image_addr"]
289 self.image_types = params["image_types"].split()
290 self.runnable_image_types = params["runnable_image_types"].split()
291 self.runnable_machine_patterns = params["runnable_machine_patterns"].split()
292 self.deployable_image_types = params["deployable_image_types"].split()
293 self.tmpdir = params["tmpdir"]
294 self.image_white_pattern = params["image_white_pattern"]
295 self.image_black_pattern = params["image_black_pattern"]
296 self.kernel_image_type = params["kernel_image_type"]
297 # for build log to show
298 self.bb_version = params["bb_version"]
299 self.target_arch = params["target_arch"]
300 self.target_os = params["target_os"]
301 self.distro_version = params["distro_version"]
302 self.tune_pkgarch = params["tune_pkgarch"]
303
304def hob_conf_filter(fn, data):
305 if fn.endswith("/local.conf"):
306 distro = data.getVar("DISTRO_HOB")
307 if distro:
308 if distro != "defaultsetup":
309 data.setVar("DISTRO", distro)
310 else:
311 data.delVar("DISTRO")
312
313 keys = ["MACHINE_HOB", "SDKMACHINE_HOB", "PACKAGE_CLASSES_HOB", \
314 "BB_NUMBER_THREADS_HOB", "PARALLEL_MAKE_HOB", "DL_DIR_HOB", \
315 "SSTATE_DIR_HOB", "SSTATE_MIRRORS_HOB", "INCOMPATIBLE_LICENSE_HOB"]
316 for key in keys:
317 var_hob = data.getVar(key)
318 if var_hob:
319 data.setVar(key.split("_HOB")[0], var_hob)
320 return
321
322 if fn.endswith("/bblayers.conf"):
323 layers = data.getVar("BBLAYERS_HOB")
324 if layers:
325 data.setVar("BBLAYERS", layers)
326 return
327
328class Builder(gtk.Window):
329
330 (INITIAL_CHECKS,
331 MACHINE_SELECTION,
332 RCPPKGINFO_POPULATING,
333 RCPPKGINFO_POPULATED,
334 BASEIMG_SELECTED,
335 RECIPE_SELECTION,
336 PACKAGE_GENERATING,
337 PACKAGE_GENERATED,
338 PACKAGE_SELECTION,
339 FAST_IMAGE_GENERATING,
340 IMAGE_GENERATING,
341 IMAGE_GENERATED,
342 MY_IMAGE_OPENED,
343 BACK,
344 END_NOOP) = range(15)
345
346 (SANITY_CHECK,
347 IMAGE_CONFIGURATION,
348 RECIPE_DETAILS,
349 BUILD_DETAILS,
350 PACKAGE_DETAILS,
351 IMAGE_DETAILS,
352 END_TAB) = range(7)
353
354 __step2page__ = {
355 INITIAL_CHECKS : SANITY_CHECK,
356 MACHINE_SELECTION : IMAGE_CONFIGURATION,
357 RCPPKGINFO_POPULATING : IMAGE_CONFIGURATION,
358 RCPPKGINFO_POPULATED : IMAGE_CONFIGURATION,
359 BASEIMG_SELECTED : IMAGE_CONFIGURATION,
360 RECIPE_SELECTION : RECIPE_DETAILS,
361 PACKAGE_GENERATING : BUILD_DETAILS,
362 PACKAGE_GENERATED : PACKAGE_DETAILS,
363 PACKAGE_SELECTION : PACKAGE_DETAILS,
364 FAST_IMAGE_GENERATING : BUILD_DETAILS,
365 IMAGE_GENERATING : BUILD_DETAILS,
366 IMAGE_GENERATED : IMAGE_DETAILS,
367 MY_IMAGE_OPENED : IMAGE_DETAILS,
368 END_NOOP : None,
369 }
370
371 SANITY_CHECK_MIN_DISPLAY_TIME = 5
372
373 def __init__(self, hobHandler, recipe_model, package_model):
374 super(Builder, self).__init__()
375
376 self.hob_image = "hob-image"
377 self.hob_toolchain = "hob-toolchain"
378
379 # handler
380 self.handler = hobHandler
381
382 # logger
383 self.logger = logging.getLogger("BitBake")
384 self.consolelog = None
385 self.current_logfile = None
386
387 # configuration and parameters
388 self.configuration = Configuration()
389 self.parameters = Parameters()
390
391 # build step
392 self.current_step = None
393 self.previous_step = None
394
395 self.stopping = False
396
397 # recipe model and package model
398 self.recipe_model = recipe_model
399 self.package_model = package_model
400
401 # Indicate whether user has customized the image
402 self.customized = False
403
404 # Indicate whether the UI is working
405 self.sensitive = True
406
407 # Indicate whether the sanity check ran
408 self.sanity_checked = False
409
410 # save parsing warnings
411 self.parsing_warnings = []
412
413 # create visual elements
414 self.create_visual_elements()
415
416 # connect the signals to functions
417 self.connect("delete-event", self.destroy_window_cb)
418 self.recipe_model.connect ("recipe-selection-changed", self.recipelist_changed_cb)
419 self.package_model.connect("package-selection-changed", self.packagelist_changed_cb)
420 self.handler.connect("config-updated", self.handler_config_updated_cb)
421 self.handler.connect("package-formats-updated", self.handler_package_formats_updated_cb)
422 self.handler.connect("parsing-started", self.handler_parsing_started_cb)
423 self.handler.connect("parsing", self.handler_parsing_cb)
424 self.handler.connect("parsing-completed", self.handler_parsing_completed_cb)
425 self.handler.build.connect("build-started", self.handler_build_started_cb)
426 self.handler.build.connect("build-succeeded", self.handler_build_succeeded_cb)
427 self.handler.build.connect("build-failed", self.handler_build_failed_cb)
428 self.handler.build.connect("build-aborted", self.handler_build_aborted_cb)
429 self.handler.build.connect("task-started", self.handler_task_started_cb)
430 self.handler.build.connect("disk-full", self.handler_disk_full_cb)
431 self.handler.build.connect("log-error", self.handler_build_failure_cb)
432 self.handler.build.connect("log-warning", self.handler_build_failure_cb)
433 self.handler.build.connect("log", self.handler_build_log_cb)
434 self.handler.build.connect("no-provider", self.handler_no_provider_cb)
435 self.handler.connect("generating-data", self.handler_generating_data_cb)
436 self.handler.connect("data-generated", self.handler_data_generated_cb)
437 self.handler.connect("command-succeeded", self.handler_command_succeeded_cb)
438 self.handler.connect("command-failed", self.handler_command_failed_cb)
439 self.handler.connect("parsing-warning", self.handler_parsing_warning_cb)
440 self.handler.connect("sanity-failed", self.handler_sanity_failed_cb)
441 self.handler.connect("recipe-populated", self.handler_recipe_populated_cb)
442 self.handler.connect("package-populated", self.handler_package_populated_cb)
443
444 self.handler.append_to_bbfiles("${TOPDIR}/recipes/images/*.bb")
445 self.initiate_new_build_async()
446
447 signal.signal(signal.SIGINT, self.event_handle_SIGINT)
448
449 def create_visual_elements(self):
450 self.set_title("Hob")
451 self.set_icon_name("applications-development")
452 self.set_resizable(True)
453
454 try:
455 window_width = self.get_screen().get_width()
456 window_height = self.get_screen().get_height()
457 except AttributeError:
458 print "Please set DISPLAY variable before running Hob."
459 sys.exit(1)
460
461 if window_width >= hwc.MAIN_WIN_WIDTH:
462 window_width = hwc.MAIN_WIN_WIDTH
463 window_height = hwc.MAIN_WIN_HEIGHT
464 self.set_size_request(window_width, window_height)
465
466 self.vbox = gtk.VBox(False, 0)
467 self.vbox.set_border_width(0)
468 self.add(self.vbox)
469
470 # create pages
471 self.image_configuration_page = ImageConfigurationPage(self)
472 self.recipe_details_page = RecipeSelectionPage(self)
473 self.build_details_page = BuildDetailsPage(self)
474 self.package_details_page = PackageSelectionPage(self)
475 self.image_details_page = ImageDetailsPage(self)
476 self.sanity_check_page = SanityCheckPage(self)
477 self.display_sanity_check = False
478 self.sanity_check_post_func = False
479 self.had_network_error = False
480
481 self.nb = gtk.Notebook()
482 self.nb.set_show_tabs(False)
483 self.nb.insert_page(self.sanity_check_page, None, self.SANITY_CHECK)
484 self.nb.insert_page(self.image_configuration_page, None, self.IMAGE_CONFIGURATION)
485 self.nb.insert_page(self.recipe_details_page, None, self.RECIPE_DETAILS)
486 self.nb.insert_page(self.build_details_page, None, self.BUILD_DETAILS)
487 self.nb.insert_page(self.package_details_page, None, self.PACKAGE_DETAILS)
488 self.nb.insert_page(self.image_details_page, None, self.IMAGE_DETAILS)
489 self.vbox.pack_start(self.nb, expand=True, fill=True)
490
491 self.show_all()
492 self.nb.set_current_page(0)
493
494 def sanity_check_timeout(self):
495 # The minimum time for showing the 'sanity check' page has passe
496 # If someone set the 'sanity_check_post_step' meanwhile, execute it now
497 self.display_sanity_check = False
498 if self.sanity_check_post_func:
499 temp = self.sanity_check_post_func
500 self.sanity_check_post_func = None
501 temp()
502 return False
503
504 def show_sanity_check_page(self):
505 # This window must stay on screen for at least 5 seconds, according to the design document
506 self.nb.set_current_page(self.SANITY_CHECK)
507 self.sanity_check_post_step = None
508 self.display_sanity_check = True
509 self.sanity_check_page.start()
510 gobject.timeout_add(self.SANITY_CHECK_MIN_DISPLAY_TIME * 1000, self.sanity_check_timeout)
511
512 def execute_after_sanity_check(self, func):
513 if not self.display_sanity_check:
514 func()
515 else:
516 self.sanity_check_post_func = func
517
518 def generate_configuration(self):
519 if not self.sanity_checked:
520 self.show_sanity_check_page()
521 self.handler.generate_configuration()
522
523 def initiate_new_build_async(self):
524 self.configuration.selected_image = None
525 self.switch_page(self.MACHINE_SELECTION)
526 self.handler.init_cooker()
527 self.handler.set_extra_inherit("image_types")
528 self.generate_configuration()
529
530 def update_config_async(self):
531 self.switch_page(self.MACHINE_SELECTION)
532 self.set_user_config()
533 self.generate_configuration()
534
535 def sanity_check(self):
536 self.handler.trigger_sanity_check()
537
538 def populate_recipe_package_info_async(self):
539 self.switch_page(self.RCPPKGINFO_POPULATING)
540 # Parse recipes
541 self.set_user_config()
542 self.handler.generate_recipes()
543
544 def generate_packages_async(self, log = False):
545 self.switch_page(self.PACKAGE_GENERATING)
546 if log:
547 self.current_logfile = self.handler.get_logfile()
548 self.do_log(self.current_logfile)
549 # Build packages
550 _, all_recipes = self.recipe_model.get_selected_recipes()
551 self.set_user_config()
552 self.handler.reset_build()
553 self.handler.generate_packages(all_recipes, self.configuration.default_task)
554
555 def restore_initial_selected_packages(self):
556 self.package_model.set_selected_packages(self.configuration.initial_user_selected_packages, True)
557 self.package_model.set_selected_packages(self.configuration.initial_selected_packages)
558 for package in self.configuration.selected_packages:
559 if package not in self.configuration.initial_selected_packages:
560 self.package_model.exclude_item(self.package_model.find_path_for_item(package))
561
562 def fast_generate_image_async(self, log = False):
563 self.switch_page(self.FAST_IMAGE_GENERATING)
564 if log:
565 self.current_logfile = self.handler.get_logfile()
566 self.do_log(self.current_logfile)
567 # Build packages
568 _, all_recipes = self.recipe_model.get_selected_recipes()
569 self.set_user_config()
570 self.handler.reset_build()
571 self.handler.generate_packages(all_recipes, self.configuration.default_task)
572
573 def generate_image_async(self, cont = False):
574 self.switch_page(self.IMAGE_GENERATING)
575 self.handler.reset_build()
576 if not cont:
577 self.current_logfile = self.handler.get_logfile()
578 self.do_log(self.current_logfile)
579 # Build image
580 self.set_user_config()
581 toolchain_packages = []
582 base_image = None
583 if self.configuration.toolchain_build:
584 toolchain_packages = self.package_model.get_selected_packages_toolchain()
585 if self.configuration.selected_image == self.recipe_model.__custom_image__:
586 packages = self.package_model.get_selected_packages()
587 image = self.hob_image
588 base_image = self.configuration.initial_selected_image
589 else:
590 packages = []
591 image = self.configuration.selected_image
592 self.handler.generate_image(image,
593 base_image,
594 self.hob_toolchain,
595 packages,
596 toolchain_packages,
597 self.configuration.default_task)
598
599 def generate_new_image(self, image, description):
600 base_image = self.configuration.initial_selected_image
601 if base_image == self.recipe_model.__custom_image__:
602 base_image = None
603 packages = self.package_model.get_selected_packages()
604 self.handler.generate_new_image(image, base_image, packages, description)
605
606 def ensure_dir(self, directory):
607 self.handler.ensure_dir(directory)
608
609 def get_parameters_sync(self):
610 return self.handler.get_parameters()
611
612 def request_package_info_async(self):
613 self.handler.request_package_info()
614
615 def cancel_build_sync(self, force=False):
616 self.handler.cancel_build(force)
617
618 def cancel_parse_sync(self):
619 self.handler.cancel_parse()
620
621 def switch_page(self, next_step):
622 # Main Workflow (Business Logic)
623 self.nb.set_current_page(self.__step2page__[next_step])
624
625 if next_step == self.MACHINE_SELECTION: # init step
626 self.image_configuration_page.show_machine()
627
628 elif next_step == self.RCPPKGINFO_POPULATING:
629 # MACHINE CHANGED action or SETTINGS CHANGED
630 # show the progress bar
631 self.image_configuration_page.show_info_populating()
632
633 elif next_step == self.RCPPKGINFO_POPULATED:
634 self.image_configuration_page.show_info_populated()
635
636 elif next_step == self.BASEIMG_SELECTED:
637 self.image_configuration_page.show_baseimg_selected()
638
639 elif next_step == self.RECIPE_SELECTION:
640 if self.recipe_model.get_selected_image() == self.recipe_model.__custom_image__:
641 self.recipe_details_page.set_recipe_curr_tab(self.recipe_details_page.ALL)
642 else:
643 self.recipe_details_page.set_recipe_curr_tab(self.recipe_details_page.INCLUDED)
644
645 elif next_step == self.PACKAGE_SELECTION:
646 self.configuration.initial_selected_packages = self.configuration.selected_packages
647 self.configuration.initial_user_selected_packages = self.configuration.user_selected_packages
648 self.package_details_page.set_title("Edit packages")
649 if self.recipe_model.get_selected_image() == self.recipe_model.__custom_image__:
650 self.package_details_page.set_packages_curr_tab(self.package_details_page.ALL)
651 else:
652 self.package_details_page.set_packages_curr_tab(self.package_details_page.INCLUDED)
653 self.package_details_page.show_page(self.current_logfile)
654
655
656 elif next_step == self.PACKAGE_GENERATING or next_step == self.FAST_IMAGE_GENERATING:
657 # both PACKAGE_GENERATING and FAST_IMAGE_GENERATING share the same page
658 self.build_details_page.show_page(next_step)
659
660 elif next_step == self.PACKAGE_GENERATED:
661 self.package_details_page.set_title("Step 2 of 2: Edit packages")
662 if self.recipe_model.get_selected_image() == self.recipe_model.__custom_image__:
663 self.package_details_page.set_packages_curr_tab(self.package_details_page.ALL)
664 else:
665 self.package_details_page.set_packages_curr_tab(self.package_details_page.INCLUDED)
666 self.package_details_page.show_page(self.current_logfile)
667
668 elif next_step == self.IMAGE_GENERATING:
669 # after packages are generated, selected_packages need to
670 # be updated in package_model per selected_image in recipe_model
671 self.build_details_page.show_page(next_step)
672
673 elif next_step == self.IMAGE_GENERATED:
674 self.image_details_page.show_page(next_step)
675
676 elif next_step == self.MY_IMAGE_OPENED:
677 self.image_details_page.show_page(next_step)
678
679 self.previous_step = self.current_step
680 self.current_step = next_step
681
682 def set_user_config_proxies(self):
683 if self.configuration.enable_proxy == True:
684 self.handler.set_http_proxy(self.configuration.combine_proxy("http"))
685 self.handler.set_https_proxy(self.configuration.combine_proxy("https"))
686 self.handler.set_ftp_proxy(self.configuration.combine_proxy("ftp"))
687 self.handler.set_socks_proxy(self.configuration.combine_proxy("socks"))
688 self.handler.set_cvs_proxy(self.configuration.combine_host_only("cvs"), self.configuration.combine_port_only("cvs"))
689 elif self.configuration.enable_proxy == False:
690 self.handler.set_http_proxy("")
691 self.handler.set_https_proxy("")
692 self.handler.set_ftp_proxy("")
693 self.handler.set_socks_proxy("")
694 self.handler.set_cvs_proxy("", "")
695
696 def set_user_config_extra(self):
697 self.handler.set_rootfs_size(self.configuration.image_rootfs_size)
698 self.handler.set_extra_size(self.configuration.image_extra_size)
699 self.handler.set_incompatible_license(self.configuration.incompat_license)
700 self.handler.set_sdk_machine(self.configuration.curr_sdk_machine)
701 self.handler.set_image_fstypes(self.configuration.image_fstypes)
702 self.handler.set_extra_config(self.configuration.extra_setting)
703 self.handler.set_extra_inherit("packageinfo image_types")
704 self.set_user_config_proxies()
705
706 def set_user_config(self):
707 self.handler.reset_cooker()
708 # set bb layers
709 self.handler.set_bblayers(self.configuration.layers)
710 # set local configuration
711 self.handler.set_machine(self.configuration.curr_mach)
712 self.handler.set_package_format(self.configuration.curr_package_format)
713 self.handler.set_distro(self.configuration.curr_distro)
714 self.handler.set_dl_dir(self.configuration.dldir)
715 self.handler.set_sstate_dir(self.configuration.sstatedir)
716 self.handler.set_sstate_mirrors(self.configuration.sstatemirror)
717 self.handler.set_pmake(self.configuration.pmake)
718 self.handler.set_bbthreads(self.configuration.bbthread)
719 self.set_user_config_extra()
720
721 def update_recipe_model(self, selected_image, selected_recipes):
722 self.recipe_model.set_selected_image(selected_image)
723 self.recipe_model.set_selected_recipes(selected_recipes)
724
725 def update_package_model(self, selected_packages, user_selected_packages=None):
726 if user_selected_packages:
727 left = self.package_model.set_selected_packages(user_selected_packages, True)
728 self.configuration.user_selected_packages += left
729 left = self.package_model.set_selected_packages(selected_packages)
730 self.configuration.selected_packages += left
731
732 def update_configuration_parameters(self, params):
733 if params:
734 self.configuration.update(params)
735 self.parameters.update(params)
736
737 def reset(self):
738 self.configuration.curr_mach = ""
739 self.configuration.clear_selection()
740 self.image_configuration_page.switch_machine_combo()
741 self.switch_page(self.MACHINE_SELECTION)
742
743 # Callback Functions
744 def handler_config_updated_cb(self, handler, which, values):
745 if which == "distro":
746 self.parameters.all_distros = values
747 elif which == "machine":
748 self.parameters.all_machines = values
749 self.image_configuration_page.update_machine_combo()
750 elif which == "machine-sdk":
751 self.parameters.all_sdk_machines = values
752
753 def handler_package_formats_updated_cb(self, handler, formats):
754 self.parameters.all_package_formats = formats
755
756 def switch_to_image_configuration_helper(self):
757 self.sanity_check_page.stop()
758 self.switch_page(self.IMAGE_CONFIGURATION)
759 self.image_configuration_page.switch_machine_combo()
760
761 def show_network_error_dialog_helper(self):
762 self.sanity_check_page.stop()
763 self.show_network_error_dialog()
764
765 def handler_command_succeeded_cb(self, handler, initcmd):
766 if initcmd == self.handler.GENERATE_CONFIGURATION:
767 if not self.configuration.curr_mach:
768 self.configuration.curr_mach = self.handler.runCommand(["getVariable", "HOB_MACHINE"]) or ""
769 self.update_configuration_parameters(self.get_parameters_sync())
770 if not self.sanity_checked:
771 self.sanity_check()
772 self.sanity_checked = True
773 elif initcmd == self.handler.SANITY_CHECK:
774 if self.had_network_error:
775 self.had_network_error = False
776 self.execute_after_sanity_check(self.show_network_error_dialog_helper)
777 else:
778 # Switch to the 'image configuration' page now, but we might need
779 # to wait for the minimum display time of the sanity check page
780 self.execute_after_sanity_check(self.switch_to_image_configuration_helper)
781 elif initcmd in [self.handler.GENERATE_RECIPES,
782 self.handler.GENERATE_PACKAGES,
783 self.handler.GENERATE_IMAGE]:
784 self.update_configuration_parameters(self.get_parameters_sync())
785 self.request_package_info_async()
786 elif initcmd == self.handler.POPULATE_PACKAGEINFO:
787 if self.current_step == self.RCPPKGINFO_POPULATING:
788 self.switch_page(self.RCPPKGINFO_POPULATED)
789 self.rcppkglist_populated()
790 return
791
792 self.rcppkglist_populated()
793 if self.current_step == self.FAST_IMAGE_GENERATING:
794 self.generate_image_async(True)
795
796 def show_error_dialog(self, msg):
797 lbl = "<b>Hob found an error</b>\n"
798 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_ERROR, msg)
799 button = dialog.add_button("Close", gtk.RESPONSE_OK)
800 HobButton.style_button(button)
801 response = dialog.run()
802 dialog.destroy()
803
804 def show_warning_dialog(self):
805 dialog = ParsingWarningsDialog(title = "View warnings",
806 warnings = self.parsing_warnings,
807 parent = None,
808 flags = gtk.DIALOG_DESTROY_WITH_PARENT
809 | gtk.DIALOG_NO_SEPARATOR)
810 response = dialog.run()
811 dialog.destroy()
812
813 def show_network_error_dialog(self):
814 lbl = "<b>Hob cannot connect to the network</b>\n"
815 msg = "Please check your network connection. If you are using a proxy server, please make sure it is configured correctly."
816 lbl = lbl + "%s\n\n" % glib.markup_escape_text(msg)
817 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_ERROR)
818 button = dialog.add_button("Close", gtk.RESPONSE_OK)
819 HobButton.style_button(button)
820 button = dialog.add_button("Proxy settings", gtk.RESPONSE_CANCEL)
821 HobButton.style_button(button)
822 res = dialog.run()
823 dialog.destroy()
824 if res == gtk.RESPONSE_CANCEL:
825 res, settings_changed = self.show_simple_settings_dialog(SimpleSettingsDialog.PROXIES_PAGE_ID)
826 if not res:
827 return
828 if settings_changed:
829 self.reparse_post_adv_settings()
830
831 def handler_command_failed_cb(self, handler, msg):
832 if msg:
833 self.show_error_dialog(msg)
834 self.reset()
835
836 def handler_parsing_warning_cb(self, handler, warn_msg):
837 self.parsing_warnings.append(warn_msg)
838
839 def handler_sanity_failed_cb(self, handler, msg, network_error):
840 self.reset()
841 if network_error:
842 # Mark this in an internal field. The "network error" dialog will be
843 # shown later, when a SanityCheckPassed event will be handled
844 # (as sent by sanity.bbclass)
845 self.had_network_error = True
846 else:
847 msg = msg.replace("your local.conf", "Settings")
848 self.show_error_dialog(msg)
849 self.reset()
850
851 def window_sensitive(self, sensitive):
852 self.image_configuration_page.machine_combo.set_sensitive(sensitive)
853 self.image_configuration_page.machine_combo.child.set_sensitive(sensitive)
854 self.image_configuration_page.image_combo.set_sensitive(sensitive)
855 self.image_configuration_page.image_combo.child.set_sensitive(sensitive)
856 self.image_configuration_page.layer_button.set_sensitive(sensitive)
857 self.image_configuration_page.layer_info_icon.set_sensitive(sensitive)
858 self.image_configuration_page.toolbar.set_sensitive(sensitive)
859 self.image_configuration_page.view_adv_configuration_button.set_sensitive(sensitive)
860 self.image_configuration_page.config_build_button.set_sensitive(sensitive)
861
862 self.recipe_details_page.set_sensitive(sensitive)
863 self.package_details_page.set_sensitive(sensitive)
864 self.build_details_page.set_sensitive(sensitive)
865 self.image_details_page.set_sensitive(sensitive)
866
867 if sensitive:
868 self.window.set_cursor(None)
869 else:
870 self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
871 self.sensitive = sensitive
872
873
874 def handler_generating_data_cb(self, handler):
875 self.window_sensitive(False)
876
877 def handler_data_generated_cb(self, handler):
878 self.window_sensitive(True)
879
880 def rcppkglist_populated(self):
881 selected_image = self.configuration.selected_image
882 selected_recipes = self.configuration.selected_recipes[:]
883 selected_packages = self.configuration.selected_packages[:]
884 user_selected_packages = self.configuration.user_selected_packages[:]
885
886 self.image_configuration_page.update_image_combo(self.recipe_model, selected_image)
887 self.image_configuration_page.update_image_desc()
888 self.update_recipe_model(selected_image, selected_recipes)
889 self.update_package_model(selected_packages, user_selected_packages)
890
891 def recipelist_changed_cb(self, recipe_model):
892 self.recipe_details_page.refresh_selection()
893
894 def packagelist_changed_cb(self, package_model):
895 self.package_details_page.refresh_selection()
896
897 def handler_recipe_populated_cb(self, handler):
898 self.image_configuration_page.update_progress_bar("Populating recipes", 0.99)
899
900 def handler_package_populated_cb(self, handler):
901 self.image_configuration_page.update_progress_bar("Populating packages", 1.0)
902
903 def handler_parsing_started_cb(self, handler, message):
904 if self.current_step != self.RCPPKGINFO_POPULATING:
905 return
906
907 fraction = 0
908 if message["eventname"] == "TreeDataPreparationStarted":
909 fraction = 0.6 + fraction
910 self.image_configuration_page.stop_button.set_sensitive(False)
911 self.image_configuration_page.update_progress_bar("Generating dependency tree", fraction)
912 else:
913 self.image_configuration_page.stop_button.set_sensitive(True)
914 self.image_configuration_page.update_progress_bar(message["title"], fraction)
915
916 def handler_parsing_cb(self, handler, message):
917 if self.current_step != self.RCPPKGINFO_POPULATING:
918 return
919
920 fraction = message["current"] * 1.0/message["total"]
921 if message["eventname"] == "TreeDataPreparationProgress":
922 fraction = 0.6 + 0.38 * fraction
923 self.image_configuration_page.update_progress_bar("Generating dependency tree", fraction)
924 else:
925 fraction = 0.6 * fraction
926 self.image_configuration_page.update_progress_bar(message["title"], fraction)
927
928 def handler_parsing_completed_cb(self, handler, message):
929 if self.current_step != self.RCPPKGINFO_POPULATING:
930 return
931
932 if message["eventname"] == "TreeDataPreparationCompleted":
933 fraction = 0.98
934 else:
935 fraction = 0.6
936 self.image_configuration_page.update_progress_bar("Generating dependency tree", fraction)
937
938 def handler_build_started_cb(self, running_build):
939 if self.current_step == self.FAST_IMAGE_GENERATING:
940 fraction = 0
941 elif self.current_step == self.IMAGE_GENERATING:
942 if self.previous_step == self.FAST_IMAGE_GENERATING:
943 fraction = 0.9
944 else:
945 fraction = 0
946 elif self.current_step == self.PACKAGE_GENERATING:
947 fraction = 0
948 self.build_details_page.update_progress_bar("Build Started: ", fraction)
949 self.build_details_page.show_configurations(self.configuration, self.parameters)
950
951 def build_succeeded(self):
952 if self.current_step == self.FAST_IMAGE_GENERATING:
953 fraction = 0.9
954 elif self.current_step == self.IMAGE_GENERATING:
955 fraction = 1.0
956 version = ""
957 self.parameters.image_names = []
958 selected_image = self.recipe_model.get_selected_image()
959 if selected_image == self.recipe_model.__custom_image__:
960 if self.configuration.initial_selected_image != selected_image:
961 version = self.recipe_model.get_custom_image_version()
962 linkname = 'hob-image' + version+ "-" + self.configuration.curr_mach
963 else:
964 linkname = selected_image + '-' + self.configuration.curr_mach
965 image_extension = self.get_image_extension()
966 for image_type in self.parameters.image_types:
967 if image_type in image_extension:
968 real_types = image_extension[image_type]
969 else:
970 real_types = [image_type]
971 for real_image_type in real_types:
972 linkpath = self.parameters.image_addr + '/' + linkname + '.' + real_image_type
973 if os.path.exists(linkpath):
974 self.parameters.image_names.append(os.readlink(linkpath))
975 elif self.current_step == self.PACKAGE_GENERATING:
976 fraction = 1.0
977 self.build_details_page.update_progress_bar("Build Completed: ", fraction)
978 self.handler.build_succeeded_async()
979 self.stopping = False
980
981 if self.current_step == self.PACKAGE_GENERATING:
982 self.switch_page(self.PACKAGE_GENERATED)
983 elif self.current_step == self.IMAGE_GENERATING:
984 self.switch_page(self.IMAGE_GENERATED)
985
986 def build_failed(self):
987 if self.stopping:
988 status = "stop"
989 message = "Build stopped: "
990 fraction = self.build_details_page.progress_bar.get_fraction()
991 stop_to_next_edit = ""
992 if self.current_step == self.FAST_IMAGE_GENERATING:
993 stop_to_next_edit = "image configuration"
994 elif self.current_step == self.IMAGE_GENERATING:
995 if self.previous_step == self.FAST_IMAGE_GENERATING:
996 stop_to_next_edit = "image configuration"
997 else:
998 stop_to_next_edit = "packages"
999 elif self.current_step == self.PACKAGE_GENERATING:
1000 stop_to_next_edit = "recipes"
1001 button = self.build_details_page.show_stop_page(stop_to_next_edit.split(' ')[0])
1002 self.set_default(button)
1003 else:
1004 fail_to_next_edit = ""
1005 if self.current_step == self.FAST_IMAGE_GENERATING:
1006 fail_to_next_edit = "image configuration"
1007 fraction = 0.9
1008 elif self.current_step == self.IMAGE_GENERATING:
1009 if self.previous_step == self.FAST_IMAGE_GENERATING:
1010 fail_to_next_edit = "image configuration"
1011 else:
1012 fail_to_next_edit = "packages"
1013 fraction = 1.0
1014 elif self.current_step == self.PACKAGE_GENERATING:
1015 fail_to_next_edit = "recipes"
1016 fraction = 1.0
1017 self.build_details_page.show_fail_page(fail_to_next_edit.split(' ')[0])
1018 status = "fail"
1019 message = "Build failed: "
1020 self.build_details_page.update_progress_bar(message, fraction, status)
1021 self.build_details_page.show_back_button()
1022 self.build_details_page.hide_stop_button()
1023 self.handler.build_failed_async()
1024 self.stopping = False
1025
1026 def handler_build_succeeded_cb(self, running_build):
1027 if not self.stopping:
1028 self.build_succeeded()
1029 else:
1030 self.build_failed()
1031
1032
1033 def handler_build_failed_cb(self, running_build):
1034 self.build_failed()
1035
1036 def handler_build_aborted_cb(self, running_build):
1037 self.build_failed()
1038
1039 def handler_no_provider_cb(self, running_build, msg):
1040 dialog = CrumbsMessageDialog(self, glib.markup_escape_text(msg), gtk.STOCK_DIALOG_INFO)
1041 button = dialog.add_button("Close", gtk.RESPONSE_OK)
1042 HobButton.style_button(button)
1043 dialog.run()
1044 dialog.destroy()
1045 self.build_failed()
1046
1047 def handler_task_started_cb(self, running_build, message):
1048 fraction = message["current"] * 1.0/message["total"]
1049 title = "Build packages"
1050 if self.current_step == self.FAST_IMAGE_GENERATING:
1051 if message["eventname"] == "sceneQueueTaskStarted":
1052 fraction = 0.27 * fraction
1053 elif message["eventname"] == "runQueueTaskStarted":
1054 fraction = 0.27 + 0.63 * fraction
1055 elif self.current_step == self.IMAGE_GENERATING:
1056 title = "Build image"
1057 if self.previous_step == self.FAST_IMAGE_GENERATING:
1058 if message["eventname"] == "sceneQueueTaskStarted":
1059 fraction = 0.27 + 0.63 + 0.03 * fraction
1060 elif message["eventname"] == "runQueueTaskStarted":
1061 fraction = 0.27 + 0.63 + 0.03 + 0.07 * fraction
1062 else:
1063 if message["eventname"] == "sceneQueueTaskStarted":
1064 fraction = 0.2 * fraction
1065 elif message["eventname"] == "runQueueTaskStarted":
1066 fraction = 0.2 + 0.8 * fraction
1067 elif self.current_step == self.PACKAGE_GENERATING:
1068 if message["eventname"] == "sceneQueueTaskStarted":
1069 fraction = 0.2 * fraction
1070 elif message["eventname"] == "runQueueTaskStarted":
1071 fraction = 0.2 + 0.8 * fraction
1072 self.build_details_page.update_progress_bar(title + ": ", fraction)
1073 self.build_details_page.update_build_status(message["current"], message["total"], message["task"])
1074
1075 def handler_disk_full_cb(self, running_build):
1076 self.disk_full = True
1077
1078 def handler_build_failure_cb(self, running_build):
1079 self.build_details_page.show_issues()
1080
1081 def handler_build_log_cb(self, running_build, func, obj):
1082 if hasattr(self.logger, func):
1083 getattr(self.logger, func)(obj)
1084
1085 def destroy_window_cb(self, widget, event):
1086 if not self.sensitive:
1087 return True
1088 elif self.handler.building:
1089 self.stop_build()
1090 return True
1091 else:
1092 gtk.main_quit()
1093
1094 def event_handle_SIGINT(self, signal, frame):
1095 for w in gtk.window_list_toplevels():
1096 if w.get_modal():
1097 w.response(gtk.RESPONSE_DELETE_EVENT)
1098 sys.exit(0)
1099
1100 def build_packages(self):
1101 _, all_recipes = self.recipe_model.get_selected_recipes()
1102 if not all_recipes:
1103 lbl = "<b>No selections made</b>\nYou have not made any selections"
1104 lbl = lbl + " so there isn't anything to bake at this time."
1105 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
1106 button = dialog.add_button("Close", gtk.RESPONSE_OK)
1107 HobButton.style_button(button)
1108 dialog.run()
1109 dialog.destroy()
1110 return
1111 self.generate_packages_async(True)
1112
1113 def build_image(self):
1114 selected_packages = self.package_model.get_selected_packages()
1115 if not selected_packages:
1116 lbl = "<b>No selections made</b>\nYou have not made any selections"
1117 lbl = lbl + " so there isn't anything to bake at this time."
1118 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
1119 button = dialog.add_button("Close", gtk.RESPONSE_OK)
1120 HobButton.style_button(button)
1121 dialog.run()
1122 dialog.destroy()
1123 return
1124 self.generate_image_async(True)
1125
1126 def just_bake(self):
1127 selected_image = self.recipe_model.get_selected_image()
1128 selected_packages = self.package_model.get_selected_packages() or []
1129
1130 # If no base image and no selected packages don't build anything
1131 if not (selected_packages or selected_image != self.recipe_model.__custom_image__):
1132 lbl = "<b>No selections made</b>\nYou have not made any selections"
1133 lbl = lbl + " so there isn't anything to bake at this time."
1134 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
1135 button = dialog.add_button("Close", gtk.RESPONSE_OK)
1136 HobButton.style_button(button)
1137 dialog.run()
1138 dialog.destroy()
1139 return
1140
1141 self.fast_generate_image_async(True)
1142
1143 def show_recipe_property_dialog(self, properties):
1144 information = {}
1145 dialog = PropertyDialog(title = properties["name"] +' '+ "properties",
1146 parent = self,
1147 information = properties,
1148 flags = gtk.DIALOG_DESTROY_WITH_PARENT
1149 | gtk.DIALOG_NO_SEPARATOR)
1150
1151 dialog.set_modal(False)
1152
1153 button = dialog.add_button("Close", gtk.RESPONSE_NO)
1154 HobAltButton.style_button(button)
1155 button.connect("clicked", lambda w: dialog.destroy())
1156
1157 dialog.run()
1158
1159 def show_packages_property_dialog(self, properties):
1160 information = {}
1161 dialog = PropertyDialog(title = properties["name"] +' '+ "properties",
1162 parent = self,
1163 information = properties,
1164 flags = gtk.DIALOG_DESTROY_WITH_PARENT
1165 | gtk.DIALOG_NO_SEPARATOR)
1166
1167 dialog.set_modal(False)
1168
1169 button = dialog.add_button("Close", gtk.RESPONSE_NO)
1170 HobAltButton.style_button(button)
1171 button.connect("clicked", lambda w: dialog.destroy())
1172
1173 dialog.run()
1174
1175 def show_layer_selection_dialog(self):
1176 dialog = LayerSelectionDialog(title = "Layers",
1177 layers = copy.deepcopy(self.configuration.layers),
1178 layers_non_removable = copy.deepcopy(self.configuration.layers_non_removable),
1179 all_layers = self.parameters.all_layers,
1180 parent = self,
1181 flags = gtk.DIALOG_MODAL
1182 | gtk.DIALOG_DESTROY_WITH_PARENT
1183 | gtk.DIALOG_NO_SEPARATOR)
1184 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
1185 HobAltButton.style_button(button)
1186 button = dialog.add_button("OK", gtk.RESPONSE_YES)
1187 HobButton.style_button(button)
1188 response = dialog.run()
1189 if response == gtk.RESPONSE_YES:
1190 self.configuration.layers = dialog.layers
1191 # DO refresh layers
1192 if dialog.layers_changed:
1193 self.update_config_async()
1194 dialog.destroy()
1195
1196 def get_image_extension(self):
1197 image_extension = {}
1198 for type in self.parameters.image_types:
1199 ext = self.handler.runCommand(["getVariable", "IMAGE_EXTENSION_%s" % type])
1200 if ext:
1201 image_extension[type] = ext.split(' ')
1202
1203 return image_extension
1204
1205 def show_load_my_images_dialog(self):
1206 image_extension = self.get_image_extension()
1207 dialog = ImageSelectionDialog(self.parameters.image_addr, self.parameters.image_types,
1208 "Open My Images", self,
1209 gtk.FILE_CHOOSER_ACTION_SAVE, None,
1210 image_extension)
1211 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
1212 HobAltButton.style_button(button)
1213 button = dialog.add_button("Open", gtk.RESPONSE_YES)
1214 HobButton.style_button(button)
1215 response = dialog.run()
1216 if response == gtk.RESPONSE_YES:
1217 if not dialog.image_names:
1218 lbl = "<b>No selections made</b>\nYou have not made any selections"
1219 crumbs_dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
1220 button = crumbs_dialog.add_button("Close", gtk.RESPONSE_OK)
1221 HobButton.style_button(button)
1222 crumbs_dialog.run()
1223 crumbs_dialog.destroy()
1224 dialog.destroy()
1225 return
1226
1227 self.parameters.image_addr = dialog.image_folder
1228 self.parameters.image_names = dialog.image_names[:]
1229 self.switch_page(self.MY_IMAGE_OPENED)
1230
1231 dialog.destroy()
1232
1233 def show_adv_settings_dialog(self, tab=None):
1234 dialog = AdvancedSettingsDialog(title = "Advanced configuration",
1235 configuration = copy.deepcopy(self.configuration),
1236 all_image_types = self.parameters.image_types,
1237 all_package_formats = self.parameters.all_package_formats,
1238 all_distros = self.parameters.all_distros,
1239 all_sdk_machines = self.parameters.all_sdk_machines,
1240 max_threads = self.parameters.max_threads,
1241 parent = self,
1242 flags = gtk.DIALOG_MODAL
1243 | gtk.DIALOG_DESTROY_WITH_PARENT
1244 | gtk.DIALOG_NO_SEPARATOR)
1245 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
1246 HobAltButton.style_button(button)
1247 button = dialog.add_button("Save", gtk.RESPONSE_YES)
1248 HobButton.style_button(button)
1249 dialog.set_save_button(button)
1250 response = dialog.run()
1251 settings_changed = False
1252 if response == gtk.RESPONSE_YES:
1253 self.configuration = dialog.configuration
1254 self.configuration.save(self.handler, True) # remember settings
1255 settings_changed = dialog.settings_changed
1256 dialog.destroy()
1257 return response == gtk.RESPONSE_YES, settings_changed
1258
1259 def show_simple_settings_dialog(self, tab=None):
1260 dialog = SimpleSettingsDialog(title = "Settings",
1261 configuration = copy.deepcopy(self.configuration),
1262 all_image_types = self.parameters.image_types,
1263 all_package_formats = self.parameters.all_package_formats,
1264 all_distros = self.parameters.all_distros,
1265 all_sdk_machines = self.parameters.all_sdk_machines,
1266 max_threads = self.parameters.max_threads,
1267 parent = self,
1268 flags = gtk.DIALOG_MODAL
1269 | gtk.DIALOG_DESTROY_WITH_PARENT
1270 | gtk.DIALOG_NO_SEPARATOR,
1271 handler = self.handler)
1272 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
1273 HobAltButton.style_button(button)
1274 button = dialog.add_button("Save", gtk.RESPONSE_YES)
1275 HobButton.style_button(button)
1276 if tab:
1277 dialog.switch_to_page(tab)
1278 response = dialog.run()
1279 settings_changed = False
1280 if response == gtk.RESPONSE_YES:
1281 self.configuration = dialog.configuration
1282 self.configuration.save(self.handler, True) # remember settings
1283 settings_changed = dialog.settings_changed
1284 if dialog.proxy_settings_changed:
1285 self.set_user_config_proxies()
1286 elif dialog.proxy_test_ran:
1287 # The user might have modified the proxies in the "Proxy"
1288 # tab, which in turn made the proxy settings modify in bb.
1289 # If "Cancel" was pressed, restore the previous proxy
1290 # settings inside bb.
1291 self.set_user_config_proxies()
1292 dialog.destroy()
1293 return response == gtk.RESPONSE_YES, settings_changed
1294
1295 def reparse_post_adv_settings(self):
1296 if not self.configuration.curr_mach:
1297 self.update_config_async()
1298 else:
1299 self.configuration.clear_selection()
1300 # DO reparse recipes
1301 self.populate_recipe_package_info_async()
1302
1303 def deploy_image(self, image_name):
1304 if not image_name:
1305 lbl = "<b>Please select an image to deploy.</b>"
1306 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
1307 button = dialog.add_button("Close", gtk.RESPONSE_OK)
1308 HobButton.style_button(button)
1309 dialog.run()
1310 dialog.destroy()
1311 return
1312
1313 image_path = os.path.join(self.parameters.image_addr, image_name)
1314 dialog = DeployImageDialog(title = "Usb Image Maker",
1315 image_path = image_path,
1316 parent = self,
1317 flags = gtk.DIALOG_MODAL
1318 | gtk.DIALOG_DESTROY_WITH_PARENT
1319 | gtk.DIALOG_NO_SEPARATOR)
1320 button = dialog.add_button("Close", gtk.RESPONSE_NO)
1321 HobAltButton.style_button(button)
1322 button = dialog.add_button("Make usb image", gtk.RESPONSE_YES)
1323 HobButton.style_button(button)
1324 response = dialog.run()
1325 dialog.destroy()
1326
1327 def show_load_kernel_dialog(self):
1328 dialog = gtk.FileChooserDialog("Load Kernel Files", self,
1329 gtk.FILE_CHOOSER_ACTION_SAVE)
1330 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
1331 HobAltButton.style_button(button)
1332 button = dialog.add_button("Open", gtk.RESPONSE_YES)
1333 HobButton.style_button(button)
1334 filter = gtk.FileFilter()
1335 filter.set_name("Kernel Files")
1336 filter.add_pattern("*.bin")
1337 dialog.add_filter(filter)
1338
1339 dialog.set_current_folder(self.parameters.image_addr)
1340
1341 response = dialog.run()
1342 kernel_path = ""
1343 if response == gtk.RESPONSE_YES:
1344 kernel_path = dialog.get_filename()
1345
1346 dialog.destroy()
1347
1348 return kernel_path
1349
1350 def runqemu_image(self, image_name, kernel_name):
1351 if not image_name or not kernel_name:
1352 lbl = "<b>Please select an %s to launch in QEMU.</b>" % ("kernel" if image_name else "image")
1353 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
1354 button = dialog.add_button("Close", gtk.RESPONSE_OK)
1355 HobButton.style_button(button)
1356 dialog.run()
1357 dialog.destroy()
1358 return
1359
1360 kernel_path = os.path.join(self.parameters.image_addr, kernel_name)
1361 image_path = os.path.join(self.parameters.image_addr, image_name)
1362
1363 source_env_path = os.path.join(self.parameters.core_base, "oe-init-build-env")
1364 tmp_path = self.parameters.tmpdir
1365 cmdline = bb.ui.crumbs.utils.which_terminal()
1366 if os.path.exists(image_path) and os.path.exists(kernel_path) \
1367 and os.path.exists(source_env_path) and os.path.exists(tmp_path) \
1368 and cmdline:
1369 cmdline += "\' bash -c \"export OE_TMPDIR=" + tmp_path + "; "
1370 cmdline += "source " + source_env_path + " " + os.getcwd() + "; "
1371 cmdline += "runqemu " + kernel_path + " " + image_path + "\"\'"
1372 subprocess.Popen(shlex.split(cmdline))
1373 else:
1374 lbl = "<b>Path error</b>\nOne of your paths is wrong,"
1375 lbl = lbl + " please make sure the following paths exist:\n"
1376 lbl = lbl + "image path:" + image_path + "\n"
1377 lbl = lbl + "kernel path:" + kernel_path + "\n"
1378 lbl = lbl + "source environment path:" + source_env_path + "\n"
1379 lbl = lbl + "tmp path: " + tmp_path + "."
1380 lbl = lbl + "You may be missing either xterm or vte for terminal services."
1381 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_ERROR)
1382 button = dialog.add_button("Close", gtk.RESPONSE_OK)
1383 HobButton.style_button(button)
1384 dialog.run()
1385 dialog.destroy()
1386
1387 def show_packages(self, ask=True):
1388 _, selected_recipes = self.recipe_model.get_selected_recipes()
1389 if selected_recipes and ask:
1390 lbl = "<b>Package list may be incomplete!</b>\nDo you want to build selected recipes"
1391 lbl = lbl + " to get a full list or just view the existing packages?"
1392 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
1393 button = dialog.add_button("View packages", gtk.RESPONSE_NO)
1394 HobAltButton.style_button(button)
1395 button = dialog.add_button("Build packages", gtk.RESPONSE_YES)
1396 HobButton.style_button(button)
1397 dialog.set_default_response(gtk.RESPONSE_YES)
1398 response = dialog.run()
1399 dialog.destroy()
1400 if response == gtk.RESPONSE_YES:
1401 self.generate_packages_async(True)
1402 else:
1403 self.switch_page(self.PACKAGE_SELECTION)
1404 else:
1405 self.switch_page(self.PACKAGE_SELECTION)
1406
1407 def show_recipes(self):
1408 self.switch_page(self.RECIPE_SELECTION)
1409
1410 def show_image_details(self):
1411 self.switch_page(self.IMAGE_GENERATED)
1412
1413 def show_configuration(self):
1414 self.switch_page(self.BASEIMG_SELECTED)
1415
1416 def stop_build(self):
1417 if self.stopping:
1418 lbl = "<b>Force Stop build?</b>\nYou've already selected Stop once,"
1419 lbl = lbl + " would you like to 'Force Stop' the build?\n\n"
1420 lbl = lbl + "This will stop the build as quickly as possible but may"
1421 lbl = lbl + " well leave your build directory in an unusable state"
1422 lbl = lbl + " that requires manual steps to fix.\n"
1423 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
1424 button = dialog.add_button("Cancel", gtk.RESPONSE_CANCEL)
1425 HobAltButton.style_button(button)
1426 button = dialog.add_button("Force Stop", gtk.RESPONSE_YES)
1427 HobButton.style_button(button)
1428 else:
1429 lbl = "<b>Stop build?</b>\n\nAre you sure you want to stop this"
1430 lbl = lbl + " build?\n\n'Stop' will stop the build as soon as all in"
1431 lbl = lbl + " progress build tasks are finished. However if a"
1432 lbl = lbl + " lengthy compilation phase is in progress this may take"
1433 lbl = lbl + " some time.\n\n"
1434 lbl = lbl + "'Force Stop' will stop the build as quickly as"
1435 lbl = lbl + " possible but may well leave your build directory in an"
1436 lbl = lbl + " unusable state that requires manual steps to fix."
1437 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
1438 button = dialog.add_button("Cancel", gtk.RESPONSE_CANCEL)
1439 HobAltButton.style_button(button)
1440 button = dialog.add_button("Force stop", gtk.RESPONSE_YES)
1441 HobAltButton.style_button(button)
1442 button = dialog.add_button("Stop", gtk.RESPONSE_OK)
1443 HobButton.style_button(button)
1444 response = dialog.run()
1445 dialog.destroy()
1446 if response != gtk.RESPONSE_CANCEL:
1447 self.stopping = True
1448 if response == gtk.RESPONSE_OK:
1449 self.build_details_page.progress_bar.set_stop_title("Stopping the build....")
1450 self.build_details_page.progress_bar.set_rcstyle("stop")
1451 self.cancel_build_sync()
1452 elif response == gtk.RESPONSE_YES:
1453 self.cancel_build_sync(True)
1454
1455 def do_log(self, consolelogfile = None):
1456 if consolelogfile:
1457 bb.utils.mkdirhier(os.path.dirname(consolelogfile))
1458 if self.consolelog:
1459 self.logger.removeHandler(self.consolelog)
1460 self.consolelog = None
1461 self.consolelog = logging.FileHandler(consolelogfile)
1462 bb.msg.addDefaultlogFilter(self.consolelog)
1463 format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
1464 self.consolelog.setFormatter(format)
1465
1466 self.logger.addHandler(self.consolelog)
1467
1468 def get_topdir(self):
1469 return self.handler.get_topdir()
1470
1471 def wait(self, delay):
1472 time_start = time.time()
1473 time_end = time_start + delay
1474 while time_end > time.time():
1475 while gtk.events_pending():
1476 gtk.main_iteration()
diff --git a/bitbake/lib/bb/ui/crumbs/buildmanager.py b/bitbake/lib/bb/ui/crumbs/buildmanager.py
new file mode 100644
index 0000000000..e858d75e4c
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/buildmanager.py
@@ -0,0 +1,455 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2008 Intel Corporation
5#
6# Authored by Rob Bradford <rob@linux.intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import gtk
22import gobject
23import threading
24import os
25import datetime
26import time
27
28class BuildConfiguration:
29 """ Represents a potential *or* historic *or* concrete build. It
30 encompasses all the things that we need to tell bitbake to do to make it
31 build what we want it to build.
32
33 It also stored the metadata URL and the set of possible machines (and the
34 distros / images / uris for these. Apart from the metdata URL these are
35 not serialised to file (since they may be transient). In some ways this
36 functionality might be shifted to the loader class."""
37
38 def __init__ (self):
39 self.metadata_url = None
40
41 # Tuple of (distros, image, urls)
42 self.machine_options = {}
43
44 self.machine = None
45 self.distro = None
46 self.image = None
47 self.urls = []
48 self.extra_urls = []
49 self.extra_pkgs = []
50
51 def get_machines_model (self):
52 model = gtk.ListStore (gobject.TYPE_STRING)
53 for machine in self.machine_options.keys():
54 model.append ([machine])
55
56 return model
57
58 def get_distro_and_images_models (self, machine):
59 distro_model = gtk.ListStore (gobject.TYPE_STRING)
60
61 for distro in self.machine_options[machine][0]:
62 distro_model.append ([distro])
63
64 image_model = gtk.ListStore (gobject.TYPE_STRING)
65
66 for image in self.machine_options[machine][1]:
67 image_model.append ([image])
68
69 return (distro_model, image_model)
70
71 def get_repos (self):
72 self.urls = self.machine_options[self.machine][2]
73 return self.urls
74
75 # It might be a lot lot better if we stored these in like, bitbake conf
76 # file format.
77 @staticmethod
78 def load_from_file (filename):
79
80 conf = BuildConfiguration()
81 with open(filename, "r") as f:
82 for line in f:
83 data = line.split (";")[1]
84 if (line.startswith ("metadata-url;")):
85 conf.metadata_url = data.strip()
86 continue
87 if (line.startswith ("url;")):
88 conf.urls += [data.strip()]
89 continue
90 if (line.startswith ("extra-url;")):
91 conf.extra_urls += [data.strip()]
92 continue
93 if (line.startswith ("machine;")):
94 conf.machine = data.strip()
95 continue
96 if (line.startswith ("distribution;")):
97 conf.distro = data.strip()
98 continue
99 if (line.startswith ("image;")):
100 conf.image = data.strip()
101 continue
102
103 return conf
104
105 # Serialise to a file. This is part of the build process and we use this
106 # to be able to repeat a given build (using the same set of parameters)
107 # but also so that we can include the details of the image / machine /
108 # distro in the build manager tree view.
109 def write_to_file (self, filename):
110 f = open (filename, "w")
111
112 lines = []
113
114 if (self.metadata_url):
115 lines += ["metadata-url;%s\n" % (self.metadata_url)]
116
117 for url in self.urls:
118 lines += ["url;%s\n" % (url)]
119
120 for url in self.extra_urls:
121 lines += ["extra-url;%s\n" % (url)]
122
123 if (self.machine):
124 lines += ["machine;%s\n" % (self.machine)]
125
126 if (self.distro):
127 lines += ["distribution;%s\n" % (self.distro)]
128
129 if (self.image):
130 lines += ["image;%s\n" % (self.image)]
131
132 f.writelines (lines)
133 f.close ()
134
135class BuildResult(gobject.GObject):
136 """ Represents an historic build. Perhaps not successful. But it includes
137 things such as the files that are in the directory (the output from the
138 build) as well as a deserialised BuildConfiguration file that is stored in
139 ".conf" in the directory for the build.
140
141 This is GObject so that it can be included in the TreeStore."""
142
143 (STATE_COMPLETE, STATE_FAILED, STATE_ONGOING) = \
144 (0, 1, 2)
145
146 def __init__ (self, parent, identifier):
147 gobject.GObject.__init__ (self)
148 self.date = None
149
150 self.files = []
151 self.status = None
152 self.identifier = identifier
153 self.path = os.path.join (parent, identifier)
154
155 # Extract the date, since the directory name is of the
156 # format build-<year><month><day>-<ordinal> we can easily
157 # pull it out.
158 # TODO: Better to stat a file?
159 (_, date, revision) = identifier.split ("-")
160 print(date)
161
162 year = int (date[0:4])
163 month = int (date[4:6])
164 day = int (date[6:8])
165
166 self.date = datetime.date (year, month, day)
167
168 self.conf = None
169
170 # By default builds are STATE_FAILED unless we find a "complete" file
171 # in which case they are STATE_COMPLETE
172 self.state = BuildResult.STATE_FAILED
173 for file in os.listdir (self.path):
174 if (file.startswith (".conf")):
175 conffile = os.path.join (self.path, file)
176 self.conf = BuildConfiguration.load_from_file (conffile)
177 elif (file.startswith ("complete")):
178 self.state = BuildResult.STATE_COMPLETE
179 else:
180 self.add_file (file)
181
182 def add_file (self, file):
183 # Just add the file for now. Don't care about the type.
184 self.files += [(file, None)]
185
186class BuildManagerModel (gtk.TreeStore):
187 """ Model for the BuildManagerTreeView. This derives from gtk.TreeStore
188 but it abstracts nicely what the columns mean and the setup of the columns
189 in the model. """
190
191 (COL_IDENT, COL_DESC, COL_MACHINE, COL_DISTRO, COL_BUILD_RESULT, COL_DATE, COL_STATE) = \
192 (0, 1, 2, 3, 4, 5, 6)
193
194 def __init__ (self):
195 gtk.TreeStore.__init__ (self,
196 gobject.TYPE_STRING,
197 gobject.TYPE_STRING,
198 gobject.TYPE_STRING,
199 gobject.TYPE_STRING,
200 gobject.TYPE_OBJECT,
201 gobject.TYPE_INT64,
202 gobject.TYPE_INT)
203
204class BuildManager (gobject.GObject):
205 """ This class manages the historic builds that have been found in the
206 "results" directory but is also used for starting a new build."""
207
208 __gsignals__ = {
209 'population-finished' : (gobject.SIGNAL_RUN_LAST,
210 gobject.TYPE_NONE,
211 ()),
212 'populate-error' : (gobject.SIGNAL_RUN_LAST,
213 gobject.TYPE_NONE,
214 ())
215 }
216
217 def update_build_result (self, result, iter):
218 # Convert the date into something we can sort by.
219 date = long (time.mktime (result.date.timetuple()))
220
221 # Add a top level entry for the build
222
223 self.model.set (iter,
224 BuildManagerModel.COL_IDENT, result.identifier,
225 BuildManagerModel.COL_DESC, result.conf.image,
226 BuildManagerModel.COL_MACHINE, result.conf.machine,
227 BuildManagerModel.COL_DISTRO, result.conf.distro,
228 BuildManagerModel.COL_BUILD_RESULT, result,
229 BuildManagerModel.COL_DATE, date,
230 BuildManagerModel.COL_STATE, result.state)
231
232 # And then we use the files in the directory as the children for the
233 # top level iter.
234 for file in result.files:
235 self.model.append (iter, (None, file[0], None, None, None, date, -1))
236
237 # This function is called as an idle by the BuildManagerPopulaterThread
238 def add_build_result (self, result):
239 gtk.gdk.threads_enter()
240 self.known_builds += [result]
241
242 self.update_build_result (result, self.model.append (None))
243
244 gtk.gdk.threads_leave()
245
246 def notify_build_finished (self):
247 # This is a bit of a hack. If we have a running build running then we
248 # will have a row in the model in STATE_ONGOING. Find it and make it
249 # as if it was a proper historic build (well, it is completed now....)
250
251 # We need to use the iters here rather than the Python iterator
252 # interface to the model since we need to pass it into
253 # update_build_result
254
255 iter = self.model.get_iter_first()
256
257 while (iter):
258 (ident, state) = self.model.get(iter,
259 BuildManagerModel.COL_IDENT,
260 BuildManagerModel.COL_STATE)
261
262 if state == BuildResult.STATE_ONGOING:
263 result = BuildResult (self.results_directory, ident)
264 self.update_build_result (result, iter)
265 iter = self.model.iter_next(iter)
266
267 def notify_build_succeeded (self):
268 # Write the "complete" file so that when we create the BuildResult
269 # object we put into the model
270
271 complete_file_path = os.path.join (self.cur_build_directory, "complete")
272 f = file (complete_file_path, "w")
273 f.close()
274 self.notify_build_finished()
275
276 def notify_build_failed (self):
277 # Without a "complete" file then this will mark the build as failed:
278 self.notify_build_finished()
279
280 # This function is called as an idle
281 def emit_population_finished_signal (self):
282 gtk.gdk.threads_enter()
283 self.emit ("population-finished")
284 gtk.gdk.threads_leave()
285
286 class BuildManagerPopulaterThread (threading.Thread):
287 def __init__ (self, manager, directory):
288 threading.Thread.__init__ (self)
289 self.manager = manager
290 self.directory = directory
291
292 def run (self):
293 # For each of the "build-<...>" directories ..
294
295 if os.path.exists (self.directory):
296 for directory in os.listdir (self.directory):
297
298 if not directory.startswith ("build-"):
299 continue
300
301 build_result = BuildResult (self.directory, directory)
302 self.manager.add_build_result (build_result)
303
304 gobject.idle_add (BuildManager.emit_population_finished_signal,
305 self.manager)
306
307 def __init__ (self, server, results_directory):
308 gobject.GObject.__init__ (self)
309
310 # The builds that we've found from walking the result directory
311 self.known_builds = []
312
313 # Save out the bitbake server, we need this for issuing commands to
314 # the cooker:
315 self.server = server
316
317 # The TreeStore that we use
318 self.model = BuildManagerModel ()
319
320 # The results directory is where we create (and look for) the
321 # build-<xyz>-<n> directories. We need to populate ourselves from
322 # directory
323 self.results_directory = results_directory
324 self.populate_from_directory (self.results_directory)
325
326 def populate_from_directory (self, directory):
327 thread = BuildManager.BuildManagerPopulaterThread (self, directory)
328 thread.start()
329
330 # Come up with the name for the next build ident by combining "build-"
331 # with the date formatted as yyyymmdd and then an ordinal. We do this by
332 # an optimistic algorithm incrementing the ordinal if we find that it
333 # already exists.
334 def get_next_build_ident (self):
335 today = datetime.date.today ()
336 datestr = str (today.year) + str (today.month) + str (today.day)
337
338 revision = 0
339 test_name = "build-%s-%d" % (datestr, revision)
340 test_path = os.path.join (self.results_directory, test_name)
341
342 while (os.path.exists (test_path)):
343 revision += 1
344 test_name = "build-%s-%d" % (datestr, revision)
345 test_path = os.path.join (self.results_directory, test_name)
346
347 return test_name
348
349 # Take a BuildConfiguration and then try and build it based on the
350 # parameters of that configuration. S
351 def do_build (self, conf):
352 server = self.server
353
354 # Work out the build directory. Note we actually create the
355 # directories here since we need to write the ".conf" file. Otherwise
356 # we could have relied on bitbake's builder thread to actually make
357 # the directories as it proceeds with the build.
358 ident = self.get_next_build_ident ()
359 build_directory = os.path.join (self.results_directory,
360 ident)
361 self.cur_build_directory = build_directory
362 os.makedirs (build_directory)
363
364 conffile = os.path.join (build_directory, ".conf")
365 conf.write_to_file (conffile)
366
367 # Add a row to the model representing this ongoing build. It's kinda a
368 # fake entry. If this build completes or fails then this gets updated
369 # with the real stuff like the historic builds
370 date = long (time.time())
371 self.model.append (None, (ident, conf.image, conf.machine, conf.distro,
372 None, date, BuildResult.STATE_ONGOING))
373 try:
374 server.runCommand(["setVariable", "BUILD_IMAGES_FROM_FEEDS", 1])
375 server.runCommand(["setVariable", "MACHINE", conf.machine])
376 server.runCommand(["setVariable", "DISTRO", conf.distro])
377 server.runCommand(["setVariable", "PACKAGE_CLASSES", "package_ipk"])
378 server.runCommand(["setVariable", "BBFILES", \
379 """${OEROOT}/meta/packages/*/*.bb ${OEROOT}/meta-moblin/packages/*/*.bb"""])
380 server.runCommand(["setVariable", "TMPDIR", "${OEROOT}/build/tmp"])
381 server.runCommand(["setVariable", "IPK_FEED_URIS", \
382 " ".join(conf.get_repos())])
383 server.runCommand(["setVariable", "DEPLOY_DIR_IMAGE",
384 build_directory])
385 server.runCommand(["buildTargets", [conf.image], "rootfs"])
386
387 except Exception as e:
388 print(e)
389
390class BuildManagerTreeView (gtk.TreeView):
391 """ The tree view for the build manager. This shows the historic builds
392 and so forth. """
393
394 # We use this function to control what goes in the cell since we store
395 # the date in the model as seconds since the epoch (for sorting) and so we
396 # need to make it human readable.
397 def date_format_custom_cell_data_func (self, col, cell, model, iter):
398 date = model.get (iter, BuildManagerModel.COL_DATE)[0]
399 datestr = time.strftime("%A %d %B %Y", time.localtime(date))
400 cell.set_property ("text", datestr)
401
402 # This format function controls what goes in the cell. We use this to map
403 # the integer state to a string and also to colourise the text
404 def state_format_custom_cell_data_fun (self, col, cell, model, iter):
405 state = model.get (iter, BuildManagerModel.COL_STATE)[0]
406
407 if (state == BuildResult.STATE_ONGOING):
408 cell.set_property ("text", "Active")
409 cell.set_property ("foreground", "#000000")
410 elif (state == BuildResult.STATE_FAILED):
411 cell.set_property ("text", "Failed")
412 cell.set_property ("foreground", "#ff0000")
413 elif (state == BuildResult.STATE_COMPLETE):
414 cell.set_property ("text", "Complete")
415 cell.set_property ("foreground", "#00ff00")
416 else:
417 cell.set_property ("text", "")
418
419 def __init__ (self):
420 gtk.TreeView.__init__(self)
421
422 # Misc descriptiony thing
423 renderer = gtk.CellRendererText ()
424 col = gtk.TreeViewColumn (None, renderer,
425 text=BuildManagerModel.COL_DESC)
426 self.append_column (col)
427
428 # Machine
429 renderer = gtk.CellRendererText ()
430 col = gtk.TreeViewColumn ("Machine", renderer,
431 text=BuildManagerModel.COL_MACHINE)
432 self.append_column (col)
433
434 # distro
435 renderer = gtk.CellRendererText ()
436 col = gtk.TreeViewColumn ("Distribution", renderer,
437 text=BuildManagerModel.COL_DISTRO)
438 self.append_column (col)
439
440 # date (using a custom function for formatting the cell contents it
441 # takes epoch -> human readable string)
442 renderer = gtk.CellRendererText ()
443 col = gtk.TreeViewColumn ("Date", renderer,
444 text=BuildManagerModel.COL_DATE)
445 self.append_column (col)
446 col.set_cell_data_func (renderer,
447 self.date_format_custom_cell_data_func)
448
449 # For status.
450 renderer = gtk.CellRendererText ()
451 col = gtk.TreeViewColumn ("Status", renderer,
452 text = BuildManagerModel.COL_STATE)
453 self.append_column (col)
454 col.set_cell_data_func (renderer,
455 self.state_format_custom_cell_data_fun)
diff --git a/bitbake/lib/bb/ui/crumbs/hig/__init__.py b/bitbake/lib/bb/ui/crumbs/hig/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/__init__.py
diff --git a/bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py
new file mode 100644
index 0000000000..5542471c7b
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py
@@ -0,0 +1,340 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import hashlib
25from bb.ui.crumbs.hobwidget import HobInfoButton, HobButton
26from bb.ui.crumbs.progressbar import HobProgressBar
27from bb.ui.crumbs.hig.settingsuihelper import SettingsUIHelper
28from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
29from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog
30from bb.ui.crumbs.hig.proxydetailsdialog import ProxyDetailsDialog
31
32"""
33The following are convenience classes for implementing GNOME HIG compliant
34BitBake GUI's
35In summary: spacing = 12px, border-width = 6px
36"""
37
38class AdvancedSettingsDialog (CrumbsDialog, SettingsUIHelper):
39
40 def details_cb(self, button, parent, protocol):
41 dialog = ProxyDetailsDialog(title = protocol.upper() + " Proxy Details",
42 user = self.configuration.proxies[protocol][1],
43 passwd = self.configuration.proxies[protocol][2],
44 parent = parent,
45 flags = gtk.DIALOG_MODAL
46 | gtk.DIALOG_DESTROY_WITH_PARENT
47 | gtk.DIALOG_NO_SEPARATOR)
48 dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
49 response = dialog.run()
50 if response == gtk.RESPONSE_OK:
51 self.configuration.proxies[protocol][1] = dialog.user
52 self.configuration.proxies[protocol][2] = dialog.passwd
53 self.refresh_proxy_components()
54 dialog.destroy()
55
56 def set_save_button(self, button):
57 self.save_button = button
58
59 def rootfs_combo_changed_cb(self, rootfs_combo, all_package_format, check_hbox):
60 combo_item = self.rootfs_combo.get_active_text()
61 modified = False
62 for child in check_hbox.get_children():
63 if isinstance(child, gtk.CheckButton):
64 check_hbox.remove(child)
65 modified = True
66 for format in all_package_format:
67 if format != combo_item:
68 check_button = gtk.CheckButton(format)
69 check_hbox.pack_start(check_button, expand=False, fill=False)
70 modified = True
71 if modified:
72 check_hbox.remove(self.pkgfmt_info)
73 check_hbox.pack_start(self.pkgfmt_info, expand=False, fill=False)
74 check_hbox.show_all()
75
76 def gen_pkgfmt_widget(self, curr_package_format, all_package_format, tooltip_combo="", tooltip_extra=""):
77 pkgfmt_vbox = gtk.VBox(False, 6)
78
79 label = self.gen_label_widget("Root file system package format")
80 pkgfmt_vbox.pack_start(label, expand=False, fill=False)
81
82 rootfs_format = ""
83 if curr_package_format:
84 rootfs_format = curr_package_format.split()[0]
85
86 rootfs_format_widget, rootfs_combo = self.gen_combo_widget(rootfs_format, all_package_format, tooltip_combo)
87 pkgfmt_vbox.pack_start(rootfs_format_widget, expand=False, fill=False)
88
89 label = self.gen_label_widget("Additional package formats")
90 pkgfmt_vbox.pack_start(label, expand=False, fill=False)
91
92 check_hbox = gtk.HBox(False, 12)
93 pkgfmt_vbox.pack_start(check_hbox, expand=False, fill=False)
94 for format in all_package_format:
95 if format != rootfs_format:
96 check_button = gtk.CheckButton(format)
97 is_active = (format in curr_package_format.split())
98 check_button.set_active(is_active)
99 check_hbox.pack_start(check_button, expand=False, fill=False)
100
101 self.pkgfmt_info = HobInfoButton(tooltip_extra, self)
102 check_hbox.pack_start(self.pkgfmt_info, expand=False, fill=False)
103
104 rootfs_combo.connect("changed", self.rootfs_combo_changed_cb, all_package_format, check_hbox)
105
106 pkgfmt_vbox.show_all()
107
108 return pkgfmt_vbox, rootfs_combo, check_hbox
109
110 def __init__(self, title, configuration, all_image_types,
111 all_package_formats, all_distros, all_sdk_machines,
112 max_threads, parent, flags, buttons=None):
113 super(AdvancedSettingsDialog, self).__init__(title, parent, flags, buttons)
114
115 # class members from other objects
116 # bitbake settings from Builder.Configuration
117 self.configuration = configuration
118 self.image_types = all_image_types
119 self.all_package_formats = all_package_formats
120 self.all_distros = all_distros[:]
121 self.all_sdk_machines = all_sdk_machines
122 self.max_threads = max_threads
123
124 # class members for internal use
125 self.distro_combo = None
126 self.dldir_text = None
127 self.sstatedir_text = None
128 self.sstatemirror_text = None
129 self.bb_spinner = None
130 self.pmake_spinner = None
131 self.rootfs_size_spinner = None
132 self.extra_size_spinner = None
133 self.gplv3_checkbox = None
134 self.sdk_checkbox = None
135 self.image_types_checkbuttons = {}
136
137 self.md5 = self.config_md5()
138 self.settings_changed = False
139
140 # create visual elements on the dialog
141 self.save_button = None
142 self.create_visual_elements()
143 self.connect("response", self.response_cb)
144
145 def _get_sorted_value(self, var):
146 return " ".join(sorted(str(var).split())) + "\n"
147
148 def config_md5(self):
149 data = ""
150 data += ("PACKAGE_CLASSES: " + self.configuration.curr_package_format + '\n')
151 data += ("DISTRO: " + self._get_sorted_value(self.configuration.curr_distro))
152 data += ("IMAGE_ROOTFS_SIZE: " + self._get_sorted_value(self.configuration.image_rootfs_size))
153 data += ("IMAGE_EXTRA_SIZE: " + self._get_sorted_value(self.configuration.image_extra_size))
154 data += ("INCOMPATIBLE_LICENSE: " + self._get_sorted_value(self.configuration.incompat_license))
155 data += ("SDK_MACHINE: " + self._get_sorted_value(self.configuration.curr_sdk_machine))
156 data += ("TOOLCHAIN_BUILD: " + self._get_sorted_value(self.configuration.toolchain_build))
157 data += ("IMAGE_FSTYPES: " + self._get_sorted_value(self.configuration.image_fstypes))
158 return hashlib.md5(data).hexdigest()
159
160 def create_visual_elements(self):
161 self.nb = gtk.Notebook()
162 self.nb.set_show_tabs(True)
163 self.nb.append_page(self.create_image_types_page(), gtk.Label("Image types"))
164 self.nb.append_page(self.create_output_page(), gtk.Label("Output"))
165 self.nb.set_current_page(0)
166 self.vbox.pack_start(self.nb, expand=True, fill=True)
167 self.vbox.pack_end(gtk.HSeparator(), expand=True, fill=True)
168
169 self.show_all()
170
171 def get_num_checked_image_types(self):
172 total = 0
173 for b in self.image_types_checkbuttons.values():
174 if b.get_active():
175 total = total + 1
176 return total
177
178 def set_save_button_state(self):
179 if self.save_button:
180 self.save_button.set_sensitive(self.get_num_checked_image_types() > 0)
181
182 def image_type_checkbutton_clicked_cb(self, button):
183 self.set_save_button_state()
184 if self.get_num_checked_image_types() == 0:
185 # Show an error dialog
186 lbl = "<b>Select an image type</b>\n\nYou need to select at least one image type."
187 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
188 button = dialog.add_button("OK", gtk.RESPONSE_OK)
189 HobButton.style_button(button)
190 response = dialog.run()
191 dialog.destroy()
192
193 def create_image_types_page(self):
194 main_vbox = gtk.VBox(False, 16)
195 main_vbox.set_border_width(6)
196
197 advanced_vbox = gtk.VBox(False, 6)
198 advanced_vbox.set_border_width(6)
199
200 distro_vbox = gtk.VBox(False, 6)
201 label = self.gen_label_widget("Distro:")
202 tooltip = "Selects the Yocto Project distribution you want"
203 try:
204 i = self.all_distros.index( "defaultsetup" )
205 except ValueError:
206 i = -1
207 if i != -1:
208 self.all_distros[ i ] = "Default"
209 if self.configuration.curr_distro == "defaultsetup":
210 self.configuration.curr_distro = "Default"
211 distro_widget, self.distro_combo = self.gen_combo_widget(self.configuration.curr_distro, self.all_distros,"<b>Distro</b>" + "*" + tooltip)
212 distro_vbox.pack_start(label, expand=False, fill=False)
213 distro_vbox.pack_start(distro_widget, expand=False, fill=False)
214 main_vbox.pack_start(distro_vbox, expand=False, fill=False)
215
216
217 rows = (len(self.image_types)+1)/3
218 table = gtk.Table(rows + 1, 10, True)
219 advanced_vbox.pack_start(table, expand=False, fill=False)
220
221 tooltip = "Image file system types you want."
222 info = HobInfoButton("<b>Image types</b>" + "*" + tooltip, self)
223 label = self.gen_label_widget("Image types:")
224 align = gtk.Alignment(0, 0.5, 0, 0)
225 table.attach(align, 0, 4, 0, 1)
226 align.add(label)
227 table.attach(info, 4, 5, 0, 1)
228
229 i = 1
230 j = 1
231 for image_type in sorted(self.image_types):
232 self.image_types_checkbuttons[image_type] = gtk.CheckButton(image_type)
233 self.image_types_checkbuttons[image_type].connect("toggled", self.image_type_checkbutton_clicked_cb)
234 article = ""
235 if image_type.startswith(("a", "e", "i", "o", "u")):
236 article = "n"
237 if image_type == "live":
238 self.image_types_checkbuttons[image_type].set_tooltip_text("Build iso and hddimg images")
239 else:
240 self.image_types_checkbuttons[image_type].set_tooltip_text("Build a%s %s image" % (article, image_type))
241 table.attach(self.image_types_checkbuttons[image_type], j - 1, j + 3, i, i + 1)
242 if image_type in self.configuration.image_fstypes.split():
243 self.image_types_checkbuttons[image_type].set_active(True)
244 i += 1
245 if i > rows:
246 i = 1
247 j = j + 4
248
249 main_vbox.pack_start(advanced_vbox, expand=False, fill=False)
250 self.set_save_button_state()
251
252 return main_vbox
253
254 def create_output_page(self):
255 advanced_vbox = gtk.VBox(False, 6)
256 advanced_vbox.set_border_width(6)
257
258 advanced_vbox.pack_start(self.gen_label_widget('<span weight="bold">Package format</span>'), expand=False, fill=False)
259 sub_vbox = gtk.VBox(False, 6)
260 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False)
261 tooltip_combo = "Selects the package format used to generate rootfs."
262 tooltip_extra = "Selects extra package formats to build"
263 pkgfmt_widget, self.rootfs_combo, self.check_hbox = self.gen_pkgfmt_widget(self.configuration.curr_package_format, self.all_package_formats,"<b>Root file system package format</b>" + "*" + tooltip_combo,"<b>Additional package formats</b>" + "*" + tooltip_extra)
264 sub_vbox.pack_start(pkgfmt_widget, expand=False, fill=False)
265
266 advanced_vbox.pack_start(self.gen_label_widget('<span weight="bold">Image size</span>'), expand=False, fill=False)
267 sub_vbox = gtk.VBox(False, 6)
268 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False)
269 label = self.gen_label_widget("Image basic size (in MB)")
270 tooltip = "Defines the size for the generated image. The OpenEmbedded build system determines the final size for the generated image using an algorithm that takes into account the initial disk space used for the generated image, the Image basic size value, and the Additional free space value.\n\nFor more information, check the <a href=\"http://www.yoctoproject.org/docs/current/poky-ref-manual/poky-ref-manual.html#var-IMAGE_ROOTFS_SIZE\">Yocto Project Reference Manual</a>."
271 rootfs_size_widget, self.rootfs_size_spinner = self.gen_spinner_widget(int(self.configuration.image_rootfs_size*1.0/1024), 0, 65536,"<b>Image basic size</b>" + "*" + tooltip)
272 sub_vbox.pack_start(label, expand=False, fill=False)
273 sub_vbox.pack_start(rootfs_size_widget, expand=False, fill=False)
274
275 sub_vbox = gtk.VBox(False, 6)
276 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False)
277 label = self.gen_label_widget("Additional free space (in MB)")
278 tooltip = "Sets extra free disk space to be added to the generated image. Use this variable when you want to ensure that a specific amount of free disk space is available on a device after an image is installed and running."
279 extra_size_widget, self.extra_size_spinner = self.gen_spinner_widget(int(self.configuration.image_extra_size*1.0/1024), 0, 65536,"<b>Additional free space</b>" + "*" + tooltip)
280 sub_vbox.pack_start(label, expand=False, fill=False)
281 sub_vbox.pack_start(extra_size_widget, expand=False, fill=False)
282
283 advanced_vbox.pack_start(self.gen_label_widget('<span weight="bold">Licensing</span>'), expand=False, fill=False)
284 self.gplv3_checkbox = gtk.CheckButton("Exclude GPLv3 packages")
285 self.gplv3_checkbox.set_tooltip_text("Check this box to prevent GPLv3 packages from being included in your image")
286 if "GPLv3" in self.configuration.incompat_license.split():
287 self.gplv3_checkbox.set_active(True)
288 else:
289 self.gplv3_checkbox.set_active(False)
290 advanced_vbox.pack_start(self.gplv3_checkbox, expand=False, fill=False)
291
292 advanced_vbox.pack_start(self.gen_label_widget('<span weight="bold">SDK</span>'), expand=False, fill=False)
293 sub_hbox = gtk.HBox(False, 6)
294 advanced_vbox.pack_start(sub_hbox, expand=False, fill=False)
295 self.sdk_checkbox = gtk.CheckButton("Populate SDK")
296 tooltip = "Check this box to generate an SDK tarball that consists of the cross-toolchain and a sysroot that contains development packages for your image."
297 self.sdk_checkbox.set_tooltip_text(tooltip)
298 self.sdk_checkbox.set_active(self.configuration.toolchain_build)
299 sub_hbox.pack_start(self.sdk_checkbox, expand=False, fill=False)
300
301 tooltip = "Select the host platform for which you want to run the toolchain contained in the SDK tarball."
302 sdk_machine_widget, self.sdk_machine_combo = self.gen_combo_widget(self.configuration.curr_sdk_machine, self.all_sdk_machines,"<b>Populate SDK</b>" + "*" + tooltip)
303 sub_hbox.pack_start(sdk_machine_widget, expand=False, fill=False)
304
305 return advanced_vbox
306
307 def response_cb(self, dialog, response_id):
308 package_format = []
309 package_format.append(self.rootfs_combo.get_active_text())
310 for child in self.check_hbox:
311 if isinstance(child, gtk.CheckButton) and child.get_active():
312 package_format.append(child.get_label())
313 self.configuration.curr_package_format = " ".join(package_format)
314
315 distro = self.distro_combo.get_active_text()
316 if distro == "Default":
317 distro = "defaultsetup"
318 self.configuration.curr_distro = distro
319 self.configuration.image_rootfs_size = self.rootfs_size_spinner.get_value_as_int() * 1024
320 self.configuration.image_extra_size = self.extra_size_spinner.get_value_as_int() * 1024
321
322 self.configuration.image_fstypes = ""
323 for image_type in self.image_types:
324 if self.image_types_checkbuttons[image_type].get_active():
325 self.configuration.image_fstypes += (" " + image_type)
326 self.configuration.image_fstypes.strip()
327
328 if self.gplv3_checkbox.get_active():
329 if "GPLv3" not in self.configuration.incompat_license.split():
330 self.configuration.incompat_license += " GPLv3"
331 else:
332 if "GPLv3" in self.configuration.incompat_license.split():
333 self.configuration.incompat_license = self.configuration.incompat_license.split().remove("GPLv3")
334 self.configuration.incompat_license = " ".join(self.configuration.incompat_license or [])
335 self.configuration.incompat_license = self.configuration.incompat_license.strip()
336
337 self.configuration.toolchain_build = self.sdk_checkbox.get_active()
338 self.configuration.curr_sdk_machine = self.sdk_machine_combo.get_active_text()
339 md5 = self.config_md5()
340 self.settings_changed = (self.md5 != md5)
diff --git a/bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py
new file mode 100644
index 0000000000..c679f9a070
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py
@@ -0,0 +1,44 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24
25"""
26The following are convenience classes for implementing GNOME HIG compliant
27BitBake GUI's
28In summary: spacing = 12px, border-width = 6px
29"""
30
31class CrumbsDialog(gtk.Dialog):
32 """
33 A GNOME HIG compliant dialog widget.
34 Add buttons with gtk.Dialog.add_button or gtk.Dialog.add_buttons
35 """
36 def __init__(self, title="", parent=None, flags=0, buttons=None):
37 super(CrumbsDialog, self).__init__(title, parent, flags, buttons)
38
39 self.set_property("has-separator", False) # note: deprecated in 2.22
40
41 self.set_border_width(6)
42 self.vbox.set_property("spacing", 12)
43 self.action_area.set_property("spacing", 12)
44 self.action_area.set_property("border-width", 6)
diff --git a/bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py
new file mode 100644
index 0000000000..097ce7b027
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py
@@ -0,0 +1,95 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import glib
24import gtk
25from bb.ui.crumbs.hobwidget import HobIconChecker
26from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
27
28"""
29The following are convenience classes for implementing GNOME HIG compliant
30BitBake GUI's
31In summary: spacing = 12px, border-width = 6px
32"""
33
34class CrumbsMessageDialog(CrumbsDialog):
35 """
36 A GNOME HIG compliant dialog widget.
37 Add buttons with gtk.Dialog.add_button or gtk.Dialog.add_buttons
38 """
39 def __init__(self, parent=None, label="", icon=gtk.STOCK_INFO, msg=""):
40 super(CrumbsMessageDialog, self).__init__("", parent, gtk.DIALOG_MODAL)
41
42 self.set_border_width(6)
43 self.vbox.set_property("spacing", 12)
44 self.action_area.set_property("spacing", 12)
45 self.action_area.set_property("border-width", 6)
46
47 first_column = gtk.HBox(spacing=12)
48 first_column.set_property("border-width", 6)
49 first_column.show()
50 self.vbox.add(first_column)
51
52 self.icon = gtk.Image()
53 # We have our own Info icon which should be used in preference of the stock icon
54 self.icon_chk = HobIconChecker()
55 self.icon.set_from_stock(self.icon_chk.check_stock_icon(icon), gtk.ICON_SIZE_DIALOG)
56 self.icon.set_property("yalign", 0.00)
57 self.icon.show()
58 first_column.pack_start(self.icon, expand=False, fill=True, padding=0)
59
60 if 0 <= len(msg) < 200:
61 lbl = label + "%s" % glib.markup_escape_text(msg)
62 self.label_short = gtk.Label()
63 self.label_short.set_use_markup(True)
64 self.label_short.set_line_wrap(True)
65 self.label_short.set_markup(lbl)
66 self.label_short.set_property("yalign", 0.00)
67 self.label_short.show()
68 first_column.add(self.label_short)
69 else:
70 second_row = gtk.VBox(spacing=12)
71 second_row.set_property("border-width", 6)
72 self.label_long = gtk.Label()
73 self.label_long.set_use_markup(True)
74 self.label_long.set_line_wrap(True)
75 self.label_long.set_markup(label)
76 self.label_long.set_alignment(0.0, 0.0)
77 second_row.pack_start(self.label_long, expand=False, fill=False, padding=0)
78 self.label_long.show()
79 self.textWindow = gtk.ScrolledWindow()
80 self.textWindow.set_shadow_type(gtk.SHADOW_IN)
81 self.textWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
82 self.msgView = gtk.TextView()
83 self.msgView.set_editable(False)
84 self.msgView.set_wrap_mode(gtk.WRAP_WORD)
85 self.msgView.set_cursor_visible(False)
86 self.msgView.set_size_request(300, 300)
87 self.buf = gtk.TextBuffer()
88 self.buf.set_text(msg)
89 self.msgView.set_buffer(self.buf)
90 self.textWindow.add(self.msgView)
91 self.msgView.show()
92 second_row.add(self.textWindow)
93 self.textWindow.show()
94 first_column.add(second_row)
95 second_row.show()
diff --git a/bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py
new file mode 100644
index 0000000000..bc1efbbfaf
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py
@@ -0,0 +1,215 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import glob
24import gtk
25import gobject
26import os
27import re
28import shlex
29import subprocess
30import tempfile
31from bb.ui.crumbs.hobwidget import hic, HobButton
32from bb.ui.crumbs.progressbar import HobProgressBar
33import bb.ui.crumbs.utils
34import bb.process
35from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
36from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog
37
38"""
39The following are convenience classes for implementing GNOME HIG compliant
40BitBake GUI's
41In summary: spacing = 12px, border-width = 6px
42"""
43
44class DeployImageDialog (CrumbsDialog):
45
46 __dummy_usb__ = "--select a usb drive--"
47
48 def __init__(self, title, image_path, parent, flags, buttons=None, standalone=False):
49 super(DeployImageDialog, self).__init__(title, parent, flags, buttons)
50
51 self.image_path = image_path
52 self.standalone = standalone
53
54 self.create_visual_elements()
55 self.connect("response", self.response_cb)
56
57 def create_visual_elements(self):
58 self.set_size_request(600, 400)
59 label = gtk.Label()
60 label.set_alignment(0.0, 0.5)
61 markup = "<span font_desc='12'>The image to be written into usb drive:</span>"
62 label.set_markup(markup)
63 self.vbox.pack_start(label, expand=False, fill=False, padding=2)
64
65 table = gtk.Table(2, 10, False)
66 table.set_col_spacings(5)
67 table.set_row_spacings(5)
68 self.vbox.pack_start(table, expand=True, fill=True)
69
70 scroll = gtk.ScrolledWindow()
71 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
72 scroll.set_shadow_type(gtk.SHADOW_IN)
73 tv = gtk.TextView()
74 tv.set_editable(False)
75 tv.set_wrap_mode(gtk.WRAP_WORD)
76 tv.set_cursor_visible(False)
77 self.buf = gtk.TextBuffer()
78 self.buf.set_text(self.image_path)
79 tv.set_buffer(self.buf)
80 scroll.add(tv)
81 table.attach(scroll, 0, 10, 0, 1)
82
83 # There are 2 ways to use DeployImageDialog
84 # One way is that called by HOB when the 'Deploy Image' button is clicked
85 # The other way is that called by a standalone script.
86 # Following block of codes handles the latter way. It adds a 'Select Image' button and
87 # emit a signal when the button is clicked.
88 if self.standalone:
89 gobject.signal_new("select_image_clicked", self, gobject.SIGNAL_RUN_FIRST,
90 gobject.TYPE_NONE, ())
91 icon = gtk.Image()
92 pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_IMAGES_DISPLAY_FILE)
93 icon.set_from_pixbuf(pix_buffer)
94 button = gtk.Button("Select Image")
95 button.set_image(icon)
96 #button.set_size_request(140, 50)
97 table.attach(button, 9, 10, 1, 2, gtk.FILL, 0, 0, 0)
98 button.connect("clicked", self.select_image_button_clicked_cb)
99
100 separator = gtk.HSeparator()
101 self.vbox.pack_start(separator, expand=False, fill=False, padding=10)
102
103 self.usb_desc = gtk.Label()
104 self.usb_desc.set_alignment(0.0, 0.5)
105 markup = "<span font_desc='12'>You haven't chosen any USB drive.</span>"
106 self.usb_desc.set_markup(markup)
107
108 self.usb_combo = gtk.combo_box_new_text()
109 self.usb_combo.connect("changed", self.usb_combo_changed_cb)
110 model = self.usb_combo.get_model()
111 model.clear()
112 self.usb_combo.append_text(self.__dummy_usb__)
113 for usb in self.find_all_usb_devices():
114 self.usb_combo.append_text("/dev/" + usb)
115 self.usb_combo.set_active(0)
116 self.vbox.pack_start(self.usb_combo, expand=False, fill=False)
117 self.vbox.pack_start(self.usb_desc, expand=False, fill=False, padding=2)
118
119 self.progress_bar = HobProgressBar()
120 self.vbox.pack_start(self.progress_bar, expand=False, fill=False)
121 separator = gtk.HSeparator()
122 self.vbox.pack_start(separator, expand=False, fill=True, padding=10)
123
124 self.vbox.show_all()
125 self.progress_bar.hide()
126
127 def set_image_text_buffer(self, image_path):
128 self.buf.set_text(image_path)
129
130 def set_image_path(self, image_path):
131 self.image_path = image_path
132
133 def popen_read(self, cmd):
134 tmpout, errors = bb.process.run("%s" % cmd)
135 return tmpout.strip()
136
137 def find_all_usb_devices(self):
138 usb_devs = [ os.readlink(u)
139 for u in glob.glob('/dev/disk/by-id/usb*')
140 if not re.search(r'part\d+', u) ]
141 return [ '%s' % u[u.rfind('/')+1:] for u in usb_devs ]
142
143 def get_usb_info(self, dev):
144 return "%s %s" % \
145 (self.popen_read('cat /sys/class/block/%s/device/vendor' % dev),
146 self.popen_read('cat /sys/class/block/%s/device/model' % dev))
147
148 def select_image_button_clicked_cb(self, button):
149 self.emit('select_image_clicked')
150
151 def usb_combo_changed_cb(self, usb_combo):
152 combo_item = self.usb_combo.get_active_text()
153 if not combo_item or combo_item == self.__dummy_usb__:
154 markup = "<span font_desc='12'>You haven't chosen any USB drive.</span>"
155 self.usb_desc.set_markup(markup)
156 else:
157 markup = "<span font_desc='12'>" + self.get_usb_info(combo_item.lstrip("/dev/")) + "</span>"
158 self.usb_desc.set_markup(markup)
159
160 def response_cb(self, dialog, response_id):
161 if response_id == gtk.RESPONSE_YES:
162 lbl = ''
163 combo_item = self.usb_combo.get_active_text()
164 if combo_item and combo_item != self.__dummy_usb__ and self.image_path:
165 cmdline = bb.ui.crumbs.utils.which_terminal()
166 if cmdline:
167 tmpfile = tempfile.NamedTemporaryFile()
168 cmdline += "\"sudo dd if=" + self.image_path + \
169 " of=" + combo_item + "; echo $? > " + tmpfile.name + "\""
170 subprocess.call(shlex.split(cmdline))
171
172 if int(tmpfile.readline().strip()) == 0:
173 lbl = "<b>Deploy image successfully.</b>"
174 else:
175 lbl = "<b>Failed to deploy image.</b>\nPlease check image <b>%s</b> exists and USB device <b>%s</b> is writable." % (self.image_path, combo_item)
176 tmpfile.close()
177 else:
178 if not self.image_path:
179 lbl = "<b>No selection made.</b>\nYou have not selected an image to deploy."
180 else:
181 lbl = "<b>No selection made.</b>\nYou have not selected a USB device."
182 if len(lbl):
183 crumbs_dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
184 button = crumbs_dialog.add_button("Close", gtk.RESPONSE_OK)
185 HobButton.style_button(button)
186 crumbs_dialog.run()
187 crumbs_dialog.destroy()
188
189 def update_progress_bar(self, title, fraction, status=None):
190 self.progress_bar.update(fraction)
191 self.progress_bar.set_title(title)
192 self.progress_bar.set_rcstyle(status)
193
194 def write_file(self, ifile, ofile):
195 self.progress_bar.reset()
196 self.progress_bar.show()
197
198 f_from = os.open(ifile, os.O_RDONLY)
199 f_to = os.open(ofile, os.O_WRONLY)
200
201 total_size = os.stat(ifile).st_size
202 written_size = 0
203
204 while True:
205 buf = os.read(f_from, 1024*1024)
206 if not buf:
207 break
208 os.write(f_to, buf)
209 written_size += 1024*1024
210 self.update_progress_bar("Writing to usb:", written_size * 1.0/total_size)
211
212 self.update_progress_bar("Writing completed:", 1.0)
213 os.close(f_from)
214 os.close(f_to)
215 self.progress_bar.hide()
diff --git a/bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py b/bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py
new file mode 100644
index 0000000000..21216adc97
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py
@@ -0,0 +1,172 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import gobject
25import os
26from bb.ui.crumbs.hobwidget import HobViewTable, HobInfoButton, HobButton, HobAltButton
27from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
28from bb.ui.crumbs.hig.layerselectiondialog import LayerSelectionDialog
29
30"""
31The following are convenience classes for implementing GNOME HIG compliant
32BitBake GUI's
33In summary: spacing = 12px, border-width = 6px
34"""
35
36class ImageSelectionDialog (CrumbsDialog):
37
38 __columns__ = [{
39 'col_name' : 'Image name',
40 'col_id' : 0,
41 'col_style': 'text',
42 'col_min' : 400,
43 'col_max' : 400
44 }, {
45 'col_name' : 'Select',
46 'col_id' : 1,
47 'col_style': 'radio toggle',
48 'col_min' : 160,
49 'col_max' : 160
50 }]
51
52
53 def __init__(self, image_folder, image_types, title, parent, flags, buttons=None, image_extension = {}):
54 super(ImageSelectionDialog, self).__init__(title, parent, flags, buttons)
55 self.connect("response", self.response_cb)
56
57 self.image_folder = image_folder
58 self.image_types = image_types
59 self.image_list = []
60 self.image_names = []
61 self.image_extension = image_extension
62
63 # create visual elements on the dialog
64 self.create_visual_elements()
65
66 self.image_store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)
67 self.fill_image_store()
68
69 def create_visual_elements(self):
70 hbox = gtk.HBox(False, 6)
71
72 self.vbox.pack_start(hbox, expand=False, fill=False)
73
74 entry = gtk.Entry()
75 entry.set_text(self.image_folder)
76 table = gtk.Table(1, 10, True)
77 table.set_size_request(560, -1)
78 hbox.pack_start(table, expand=False, fill=False)
79 table.attach(entry, 0, 9, 0, 1)
80 image = gtk.Image()
81 image.set_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_BUTTON)
82 open_button = gtk.Button()
83 open_button.set_image(image)
84 open_button.connect("clicked", self.select_path_cb, self, entry)
85 table.attach(open_button, 9, 10, 0, 1)
86
87 self.image_table = HobViewTable(self.__columns__, "Images")
88 self.image_table.set_size_request(-1, 300)
89 self.image_table.connect("toggled", self.toggled_cb)
90 self.image_table.connect_group_selection(self.table_selected_cb)
91 self.image_table.connect("row-activated", self.row_actived_cb)
92 self.vbox.pack_start(self.image_table, expand=True, fill=True)
93
94 self.show_all()
95
96 def change_image_cb(self, model, path, columnid):
97 if not model:
98 return
99 iter = model.get_iter_first()
100 while iter:
101 rowpath = model.get_path(iter)
102 model[rowpath][columnid] = False
103 iter = model.iter_next(iter)
104
105 model[path][columnid] = True
106
107 def toggled_cb(self, table, cell, path, columnid, tree):
108 model = tree.get_model()
109 self.change_image_cb(model, path, columnid)
110
111 def table_selected_cb(self, selection):
112 model, paths = selection.get_selected_rows()
113 if paths:
114 self.change_image_cb(model, paths[0], 1)
115
116 def row_actived_cb(self, tab, model, path):
117 self.change_image_cb(model, path, 1)
118 self.emit('response', gtk.RESPONSE_YES)
119
120 def select_path_cb(self, action, parent, entry):
121 dialog = gtk.FileChooserDialog("", parent,
122 gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
123 text = entry.get_text()
124 dialog.set_current_folder(text if len(text) > 0 else os.getcwd())
125 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
126 HobAltButton.style_button(button)
127 button = dialog.add_button("Open", gtk.RESPONSE_YES)
128 HobButton.style_button(button)
129 response = dialog.run()
130 if response == gtk.RESPONSE_YES:
131 path = dialog.get_filename()
132 entry.set_text(path)
133 self.image_folder = path
134 self.fill_image_store()
135
136 dialog.destroy()
137
138 def fill_image_store(self):
139 self.image_list = []
140 self.image_store.clear()
141 imageset = set()
142 for root, dirs, files in os.walk(self.image_folder):
143 # ignore the sub directories
144 dirs[:] = []
145 for f in files:
146 for image_type in self.image_types:
147 if image_type in self.image_extension:
148 real_types = self.image_extension[image_type]
149 else:
150 real_types = [image_type]
151 for real_image_type in real_types:
152 if f.endswith('.' + real_image_type):
153 imageset.add(f.rsplit('.' + real_image_type)[0].rsplit('.rootfs')[0])
154 self.image_list.append(f)
155
156 for image in imageset:
157 self.image_store.set(self.image_store.append(), 0, image, 1, False)
158
159 self.image_table.set_model(self.image_store)
160
161 def response_cb(self, dialog, response_id):
162 self.image_names = []
163 if response_id == gtk.RESPONSE_YES:
164 iter = self.image_store.get_iter_first()
165 while iter:
166 path = self.image_store.get_path(iter)
167 if self.image_store[path][1]:
168 for f in self.image_list:
169 if f.startswith(self.image_store[path][0] + '.'):
170 self.image_names.append(f)
171 break
172 iter = self.image_store.iter_next(iter)
diff --git a/bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py b/bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py
new file mode 100644
index 0000000000..783ee73c7a
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py
@@ -0,0 +1,296 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import gobject
25import os
26import tempfile
27from bb.ui.crumbs.hobwidget import hic, HobButton, HobAltButton
28from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
29from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog
30
31"""
32The following are convenience classes for implementing GNOME HIG compliant
33BitBake GUI's
34In summary: spacing = 12px, border-width = 6px
35"""
36
37class CellRendererPixbufActivatable(gtk.CellRendererPixbuf):
38 """
39 A custom CellRenderer implementation which is activatable
40 so that we can handle user clicks
41 """
42 __gsignals__ = { 'clicked' : (gobject.SIGNAL_RUN_LAST,
43 gobject.TYPE_NONE,
44 (gobject.TYPE_STRING,)), }
45
46 def __init__(self):
47 gtk.CellRendererPixbuf.__init__(self)
48 self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
49 self.set_property('follow-state', True)
50
51 """
52 Respond to a user click on a cell
53 """
54 def do_activate(self, even, widget, path, background_area, cell_area, flags):
55 self.emit('clicked', path)
56
57#
58# LayerSelectionDialog
59#
60class LayerSelectionDialog (CrumbsDialog):
61
62 TARGETS = [
63 ("MY_TREE_MODEL_ROW", gtk.TARGET_SAME_WIDGET, 0),
64 ("text/plain", 0, 1),
65 ("TEXT", 0, 2),
66 ("STRING", 0, 3),
67 ]
68
69 def gen_label_widget(self, content):
70 label = gtk.Label()
71 label.set_alignment(0, 0)
72 label.set_markup(content)
73 label.show()
74 return label
75
76 def layer_widget_toggled_cb(self, cell, path, layer_store):
77 name = layer_store[path][0]
78 toggle = not layer_store[path][1]
79 layer_store[path][1] = toggle
80
81 def layer_widget_add_clicked_cb(self, action, layer_store, parent):
82 dialog = gtk.FileChooserDialog("Add new layer", parent,
83 gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
84 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
85 HobAltButton.style_button(button)
86 button = dialog.add_button("Open", gtk.RESPONSE_YES)
87 HobButton.style_button(button)
88 label = gtk.Label("Select the layer you wish to add")
89 label.show()
90 dialog.set_extra_widget(label)
91 response = dialog.run()
92 path = dialog.get_filename()
93 dialog.destroy()
94
95 lbl = "<b>Error</b>\nUnable to load layer <i>%s</i> because " % path
96 if response == gtk.RESPONSE_YES:
97 import os
98 import os.path
99 layers = []
100 it = layer_store.get_iter_first()
101 while it:
102 layers.append(layer_store.get_value(it, 0))
103 it = layer_store.iter_next(it)
104
105 if not path:
106 lbl += "it is an invalid path."
107 elif not os.path.exists(path+"/conf/layer.conf"):
108 lbl += "there is no layer.conf inside the directory."
109 elif path in layers:
110 lbl += "it is already in loaded layers."
111 else:
112 layer_store.append([path])
113 return
114 dialog = CrumbsMessageDialog(parent, lbl)
115 dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
116 response = dialog.run()
117 dialog.destroy()
118
119 def layer_widget_del_clicked_cb(self, action, tree_selection, layer_store):
120 model, iter = tree_selection.get_selected()
121 if iter:
122 layer_store.remove(iter)
123
124
125 def gen_layer_widget(self, layers, layers_avail, window, tooltip=""):
126 hbox = gtk.HBox(False, 6)
127
128 layer_tv = gtk.TreeView()
129 layer_tv.set_rules_hint(True)
130 layer_tv.set_headers_visible(False)
131 tree_selection = layer_tv.get_selection()
132 tree_selection.set_mode(gtk.SELECTION_SINGLE)
133
134 # Allow enable drag and drop of rows including row move
135 layer_tv.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
136 self.TARGETS,
137 gtk.gdk.ACTION_DEFAULT|
138 gtk.gdk.ACTION_MOVE)
139 layer_tv.enable_model_drag_dest(self.TARGETS,
140 gtk.gdk.ACTION_DEFAULT)
141 layer_tv.connect("drag_data_get", self.drag_data_get_cb)
142 layer_tv.connect("drag_data_received", self.drag_data_received_cb)
143
144 col0= gtk.TreeViewColumn('Path')
145 cell0 = gtk.CellRendererText()
146 cell0.set_padding(5,2)
147 col0.pack_start(cell0, True)
148 col0.set_cell_data_func(cell0, self.draw_layer_path_cb)
149 layer_tv.append_column(col0)
150
151 scroll = gtk.ScrolledWindow()
152 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
153 scroll.set_shadow_type(gtk.SHADOW_IN)
154 scroll.add(layer_tv)
155
156 table_layer = gtk.Table(2, 10, False)
157 hbox.pack_start(table_layer, expand=True, fill=True)
158
159 table_layer.attach(scroll, 0, 10, 0, 1)
160
161 layer_store = gtk.ListStore(gobject.TYPE_STRING)
162 for layer in layers:
163 layer_store.append([layer])
164
165 col1 = gtk.TreeViewColumn('Enabled')
166 layer_tv.append_column(col1)
167
168 cell1 = CellRendererPixbufActivatable()
169 cell1.set_fixed_size(-1,35)
170 cell1.connect("clicked", self.del_cell_clicked_cb, layer_store)
171 col1.pack_start(cell1, True)
172 col1.set_cell_data_func(cell1, self.draw_delete_button_cb, layer_tv)
173
174 add_button = gtk.Button()
175 add_button.set_relief(gtk.RELIEF_NONE)
176 box = gtk.HBox(False, 6)
177 box.show()
178 add_button.add(box)
179 add_button.connect("enter-notify-event", self.add_hover_cb)
180 add_button.connect("leave-notify-event", self.add_leave_cb)
181 self.im = gtk.Image()
182 self.im.set_from_file(hic.ICON_INDI_ADD_FILE)
183 self.im.show()
184 box.pack_start(self.im, expand=False, fill=False, padding=6)
185 lbl = gtk.Label("Add layer")
186 lbl.set_alignment(0.0, 0.5)
187 lbl.show()
188 box.pack_start(lbl, expand=True, fill=True, padding=6)
189 add_button.connect("clicked", self.layer_widget_add_clicked_cb, layer_store, window)
190 table_layer.attach(add_button, 0, 10, 1, 2, gtk.EXPAND | gtk.FILL, 0, 0, 6)
191 layer_tv.set_model(layer_store)
192
193 hbox.show_all()
194
195 return hbox, layer_store
196
197 def drag_data_get_cb(self, treeview, context, selection, target_id, etime):
198 treeselection = treeview.get_selection()
199 model, iter = treeselection.get_selected()
200 data = model.get_value(iter, 0)
201 selection.set(selection.target, 8, data)
202
203 def drag_data_received_cb(self, treeview, context, x, y, selection, info, etime):
204 model = treeview.get_model()
205 data = selection.data
206 drop_info = treeview.get_dest_row_at_pos(x, y)
207 if drop_info:
208 path, position = drop_info
209 iter = model.get_iter(path)
210 if (position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
211 model.insert_before(iter, [data])
212 else:
213 model.insert_after(iter, [data])
214 else:
215 model.append([data])
216 if context.action == gtk.gdk.ACTION_MOVE:
217 context.finish(True, True, etime)
218 return
219
220 def add_hover_cb(self, button, event):
221 self.im.set_from_file(hic.ICON_INDI_ADD_HOVER_FILE)
222
223 def add_leave_cb(self, button, event):
224 self.im.set_from_file(hic.ICON_INDI_ADD_FILE)
225
226 def __init__(self, title, layers, layers_non_removable, all_layers, parent, flags, buttons=None):
227 super(LayerSelectionDialog, self).__init__(title, parent, flags, buttons)
228
229 # class members from other objects
230 self.layers = layers
231 self.layers_non_removable = layers_non_removable
232 self.all_layers = all_layers
233 self.layers_changed = False
234
235 # icon for remove button in TreeView
236 im = gtk.Image()
237 im.set_from_file(hic.ICON_INDI_REMOVE_FILE)
238 self.rem_icon = im.get_pixbuf()
239
240 # class members for internal use
241 self.layer_store = None
242
243 # create visual elements on the dialog
244 self.create_visual_elements()
245 self.connect("response", self.response_cb)
246
247 def create_visual_elements(self):
248 layer_widget, self.layer_store = self.gen_layer_widget(self.layers, self.all_layers, self, None)
249 layer_widget.set_size_request(450, 250)
250 self.vbox.pack_start(layer_widget, expand=True, fill=True)
251 self.show_all()
252
253 def response_cb(self, dialog, response_id):
254 model = self.layer_store
255 it = model.get_iter_first()
256 layers = []
257 while it:
258 layers.append(model.get_value(it, 0))
259 it = model.iter_next(it)
260
261 self.layers_changed = (self.layers != layers)
262 self.layers = layers
263
264 """
265 A custom cell_data_func to draw a delete 'button' in the TreeView for layers
266 other than the meta layer. The deletion of which is prevented so that the
267 user can't shoot themselves in the foot too badly.
268 """
269 def draw_delete_button_cb(self, col, cell, model, it, tv):
270 path = model.get_value(it, 0)
271 if path in self.layers_non_removable:
272 cell.set_sensitive(False)
273 cell.set_property('pixbuf', None)
274 cell.set_property('mode', gtk.CELL_RENDERER_MODE_INERT)
275 else:
276 cell.set_property('pixbuf', self.rem_icon)
277 cell.set_sensitive(True)
278 cell.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
279
280 return True
281
282 """
283 A custom cell_data_func to write an extra message into the layer path cell
284 for the meta layer. We should inform the user that they can't remove it for
285 their own safety.
286 """
287 def draw_layer_path_cb(self, col, cell, model, it):
288 path = model.get_value(it, 0)
289 if path in self.layers_non_removable:
290 cell.set_property('markup', "<b>It cannot be removed</b>\n%s" % path)
291 else:
292 cell.set_property('text', path)
293
294 def del_cell_clicked_cb(self, cell, path, model):
295 it = model.get_iter_from_string(path)
296 model.remove(it)
diff --git a/bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py
new file mode 100644
index 0000000000..33bac39db8
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py
@@ -0,0 +1,163 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Cristiana Voicu <cristiana.voicu@intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import gtk
22import gobject
23from bb.ui.crumbs.hobwidget import HobAltButton
24from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
25
26"""
27The following are convenience classes for implementing GNOME HIG compliant
28BitBake GUI's
29In summary: spacing = 12px, border-width = 6px
30"""
31
32#
33# ParsingWarningsDialog
34#
35class ParsingWarningsDialog (CrumbsDialog):
36
37 def __init__(self, title, warnings, parent, flags, buttons=None):
38 super(ParsingWarningsDialog, self).__init__(title, parent, flags, buttons)
39
40 self.warnings = warnings
41 self.warning_on = 0
42 self.warn_nb = len(warnings)
43
44 # create visual elements on the dialog
45 self.create_visual_elements()
46
47 def cancel_button_cb(self, button):
48 self.destroy()
49
50 def previous_button_cb(self, button):
51 self.warning_on = self.warning_on - 1
52 self.refresh_components()
53
54 def next_button_cb(self, button):
55 self.warning_on = self.warning_on + 1
56 self.refresh_components()
57
58 def refresh_components(self):
59 lbl = self.warnings[self.warning_on]
60 #when the warning text has more than 400 chars, it uses a scroll bar
61 if 0<= len(lbl) < 400:
62 self.warning_label.set_size_request(320, 230)
63 self.warning_label.set_use_markup(True)
64 self.warning_label.set_line_wrap(True)
65 self.warning_label.set_markup(lbl)
66 self.warning_label.set_property("yalign", 0.00)
67 else:
68 self.textWindow.set_shadow_type(gtk.SHADOW_IN)
69 self.textWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
70 self.msgView = gtk.TextView()
71 self.msgView.set_editable(False)
72 self.msgView.set_wrap_mode(gtk.WRAP_WORD)
73 self.msgView.set_cursor_visible(False)
74 self.msgView.set_size_request(320, 230)
75 self.buf = gtk.TextBuffer()
76 self.buf.set_text(lbl)
77 self.msgView.set_buffer(self.buf)
78 self.textWindow.add(self.msgView)
79 self.msgView.show()
80
81 if self.warning_on==0:
82 self.previous_button.set_sensitive(False)
83 else:
84 self.previous_button.set_sensitive(True)
85
86 if self.warning_on==self.warn_nb-1:
87 self.next_button.set_sensitive(False)
88 else:
89 self.next_button.set_sensitive(True)
90
91 if self.warn_nb>1:
92 self.heading = "Warning " + str(self.warning_on + 1) + " of " + str(self.warn_nb)
93 self.heading_label.set_markup('<span weight="bold">%s</span>' % self.heading)
94 else:
95 self.heading = "Warning"
96 self.heading_label.set_markup('<span weight="bold">%s</span>' % self.heading)
97
98 self.show_all()
99
100 if 0<= len(lbl) < 400:
101 self.textWindow.hide()
102 else:
103 self.warning_label.hide()
104
105 def create_visual_elements(self):
106 self.set_size_request(350, 350)
107 self.heading_label = gtk.Label()
108 self.heading_label.set_alignment(0, 0)
109 self.warning_label = gtk.Label()
110 self.warning_label.set_selectable(True)
111 self.warning_label.set_alignment(0, 0)
112 self.textWindow = gtk.ScrolledWindow()
113
114 table = gtk.Table(1, 10, False)
115
116 cancel_button = gtk.Button()
117 cancel_button.set_label("Close")
118 cancel_button.connect("clicked", self.cancel_button_cb)
119 cancel_button.set_size_request(110, 30)
120
121 self.previous_button = gtk.Button()
122 image1 = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON)
123 image1.show()
124 box = gtk.HBox(False, 6)
125 box.show()
126 self.previous_button.add(box)
127 lbl = gtk.Label("Previous")
128 lbl.show()
129 box.pack_start(image1, expand=False, fill=False, padding=3)
130 box.pack_start(lbl, expand=True, fill=True, padding=3)
131 self.previous_button.connect("clicked", self.previous_button_cb)
132 self.previous_button.set_size_request(110, 30)
133
134 self.next_button = gtk.Button()
135 image2 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON)
136 image2.show()
137 box = gtk.HBox(False, 6)
138 box.show()
139 self.next_button.add(box)
140 lbl = gtk.Label("Next")
141 lbl.show()
142 box.pack_start(lbl, expand=True, fill=True, padding=3)
143 box.pack_start(image2, expand=False, fill=False, padding=3)
144 self.next_button.connect("clicked", self.next_button_cb)
145 self.next_button.set_size_request(110, 30)
146
147 #when there more than one warning, we need "previous" and "next" button
148 if self.warn_nb>1:
149 self.vbox.pack_start(self.heading_label, expand=False, fill=False)
150 self.vbox.pack_start(self.warning_label, expand=False, fill=False)
151 self.vbox.pack_start(self.textWindow, expand=False, fill=False)
152 table.attach(cancel_button, 6, 7, 0, 1, xoptions=gtk.SHRINK)
153 table.attach(self.previous_button, 7, 8, 0, 1, xoptions=gtk.SHRINK)
154 table.attach(self.next_button, 8, 9, 0, 1, xoptions=gtk.SHRINK)
155 self.vbox.pack_end(table, expand=False, fill=False)
156 else:
157 self.vbox.pack_start(self.heading_label, expand=False, fill=False)
158 self.vbox.pack_start(self.warning_label, expand=False, fill=False)
159 self.vbox.pack_start(self.textWindow, expand=False, fill=False)
160 cancel_button = self.add_button("Close", gtk.RESPONSE_CANCEL)
161 HobAltButton.style_button(cancel_button)
162
163 self.refresh_components()
diff --git a/bitbake/lib/bb/ui/crumbs/hig/propertydialog.py b/bitbake/lib/bb/ui/crumbs/hig/propertydialog.py
new file mode 100644
index 0000000000..bc40741bf2
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/propertydialog.py
@@ -0,0 +1,451 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2013 Intel Corporation
5#
6# Authored by Andrei Dinu <andrei.adrianx.dinu@intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import string
22import gtk
23import gobject
24import os
25import tempfile
26import glib
27from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
28from bb.ui.crumbs.hig.settingsuihelper import SettingsUIHelper
29from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog
30from bb.ui.crumbs.hig.layerselectiondialog import LayerSelectionDialog
31
32"""
33The following are convenience classes for implementing GNOME HIG compliant
34BitBake GUI's
35In summary: spacing = 12px, border-width = 6px
36"""
37
38class PropertyDialog(CrumbsDialog):
39
40 def __init__(self, title, parent, information, flags, buttons=None):
41
42 super(PropertyDialog, self).__init__(title, parent, flags, buttons)
43
44 self.properties = information
45
46 if len(self.properties) == 10:
47 self.create_recipe_visual_elements()
48 elif len(self.properties) == 5:
49 self.create_package_visual_elements()
50 else:
51 self.create_information_visual_elements()
52
53
54 def create_information_visual_elements(self):
55
56 HOB_ICON_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ("icons/"))
57 ICON_PACKAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_display.png'))
58
59 self.set_resizable(False)
60
61 self.table = gtk.Table(1,1,False)
62 self.table.set_row_spacings(0)
63 self.table.set_col_spacings(0)
64
65 self.image = gtk.Image()
66 self.image.set_from_file(ICON_PACKAGES_DISPLAY_FILE)
67 self.image.set_property("xalign",0)
68 #self.vbox.add(self.image)
69
70 image_info = self.properties.split("*")[0]
71 info = self.properties.split("*")[1]
72
73 vbox = gtk.VBox(True, spacing=30)
74
75 self.label_short = gtk.Label()
76 self.label_short.set_line_wrap(False)
77 self.label_short.set_markup(image_info)
78 self.label_short.set_property("xalign", 0)
79
80 self.info_label = gtk.Label()
81 self.info_label.set_line_wrap(True)
82 self.info_label.set_markup(info)
83 self.info_label.set_property("yalign", 0.5)
84
85 self.table.attach(self.image, 0,1,0,1, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL,xpadding=5,ypadding=5)
86 self.table.attach(self.label_short, 0,1,0,1, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL,xpadding=40,ypadding=5)
87 self.table.attach(self.info_label, 0,1,1,2, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL,xpadding=40,ypadding=10)
88
89 self.vbox.add(self.table)
90 self.connect('delete-event', lambda w, e: self.destroy() or True)
91
92 def treeViewTooltip( self, widget, e, tooltips, cell, emptyText="" ):
93 try:
94 (path,col,x,y) = widget.get_path_at_pos( int(e.x), int(e.y) )
95 it = widget.get_model().get_iter(path)
96 value = widget.get_model().get_value(it,cell)
97 if value in self.tooltip_items:
98 tooltips.set_tip(widget, self.tooltip_items[value])
99 tooltips.enable()
100 else:
101 tooltips.set_tip(widget, emptyText)
102 except:
103 tooltips.set_tip(widget, emptyText)
104
105
106 def create_package_visual_elements(self):
107
108 name = self.properties['name']
109 binb = self.properties['binb']
110 size = self.properties['size']
111 recipe = self.properties['recipe']
112 file_list = self.properties['files_list']
113
114 file_list = file_list.strip("{}'")
115 files_temp = ''
116 paths_temp = ''
117 files_binb = []
118 paths_binb = []
119
120 self.tooltip_items = {}
121
122 self.set_resizable(False)
123
124 #cleaning out the recipe variable
125 recipe = recipe.split("+")[0]
126
127 vbox = gtk.VBox(True,spacing = 0)
128
129 ###################################### NAME ROW + COL #################################
130
131 self.label_short = gtk.Label()
132 self.label_short.set_size_request(300,-1)
133 self.label_short.set_selectable(True)
134 self.label_short.set_line_wrap(True)
135 self.label_short.set_markup("<span weight=\"bold\">Name: </span>" + name)
136 self.label_short.set_property("xalign", 0)
137
138 self.vbox.add(self.label_short)
139
140 ###################################### SIZE ROW + COL ######################################
141
142 self.label_short = gtk.Label()
143 self.label_short.set_size_request(300,-1)
144 self.label_short.set_selectable(True)
145 self.label_short.set_line_wrap(True)
146 self.label_short.set_markup("<span weight=\"bold\">Size: </span>" + size)
147 self.label_short.set_property("xalign", 0)
148
149 self.vbox.add(self.label_short)
150
151 ##################################### RECIPE ROW + COL #########################################
152
153 self.label_short = gtk.Label()
154 self.label_short.set_size_request(300,-1)
155 self.label_short.set_selectable(True)
156 self.label_short.set_line_wrap(True)
157 self.label_short.set_markup("<span weight=\"bold\">Recipe: </span>" + recipe)
158 self.label_short.set_property("xalign", 0)
159
160 self.vbox.add(self.label_short)
161
162 ##################################### BINB ROW + COL #######################################
163
164 if binb != '':
165 self.label_short = gtk.Label()
166 self.label_short.set_selectable(True)
167 self.label_short.set_line_wrap(True)
168 self.label_short.set_markup("<span weight=\"bold\">Brought in by: </span>")
169 self.label_short.set_property("xalign", 0)
170
171 self.label_info = gtk.Label()
172 self.label_info.set_size_request(300,-1)
173 self.label_info.set_selectable(True)
174 self.label_info.set_line_wrap(True)
175 self.label_info.set_markup(binb)
176 self.label_info.set_property("xalign", 0)
177
178 self.vbox.add(self.label_short)
179 self.vbox.add(self.label_info)
180
181 #################################### FILES BROUGHT BY PACKAGES ###################################
182
183 if file_list != '':
184
185 self.textWindow = gtk.ScrolledWindow()
186 self.textWindow.set_shadow_type(gtk.SHADOW_IN)
187 self.textWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
188 self.textWindow.set_size_request(100, 170)
189
190 sstatemirrors_store = gtk.ListStore(str)
191
192 self.sstatemirrors_tv = gtk.TreeView()
193 self.sstatemirrors_tv.set_rules_hint(True)
194 self.sstatemirrors_tv.set_headers_visible(True)
195 self.textWindow.add(self.sstatemirrors_tv)
196
197 self.cell1 = gtk.CellRendererText()
198 col1 = gtk.TreeViewColumn('Package files', self.cell1)
199 col1.set_cell_data_func(self.cell1, self.regex_field)
200 self.sstatemirrors_tv.append_column(col1)
201
202 for items in file_list.split(']'):
203 if len(items) > 1:
204 paths_temp = items.split(":")[0]
205 paths_binb.append(paths_temp.strip(" ,'"))
206 files_temp = items.split(":")[1]
207 files_binb.append(files_temp.strip(" ['"))
208
209 unsorted_list = []
210
211 for items in range(len(paths_binb)):
212 if len(files_binb[items]) > 1:
213 for aduse in (files_binb[items].split(",")):
214 unsorted_list.append(paths_binb[items].split(name)[len(paths_binb[items].split(name))-1] + '/' + aduse.strip(" '"))
215
216
217 unsorted_list.sort()
218 for items in unsorted_list:
219 temp = items
220 while len(items) > 35:
221 items = items[:len(items)/2] + "" + items[len(items)/2+1:]
222 if len(items) == 35:
223 items = items[:len(items)/2] + "..." + items[len(items)/2+3:]
224 self.tooltip_items[items] = temp
225
226 sstatemirrors_store.append([str(items)])
227
228
229 self.sstatemirrors_tv.set_model(sstatemirrors_store)
230
231 tips = gtk.Tooltips()
232 tips.set_tip(self.sstatemirrors_tv, "")
233 self.sstatemirrors_tv.connect("motion-notify-event", self.treeViewTooltip, tips, 0)
234 self.sstatemirrors_tv.set_events(gtk.gdk.POINTER_MOTION_MASK)
235
236 self.vbox.add(self.textWindow)
237
238 self.vbox.show_all()
239
240
241 def regex_field(self, column, cell, model, iter):
242 cell.set_property('text', model.get_value(iter, 0))
243 return
244
245
246 def create_recipe_visual_elements(self):
247
248 summary = self.properties['summary']
249 name = self.properties['name']
250 version = self.properties['version']
251 revision = self.properties['revision']
252 binb = self.properties['binb']
253 group = self.properties['group']
254 license = self.properties['license']
255 homepage = self.properties['homepage']
256 bugtracker = self.properties['bugtracker']
257 description = self.properties['description']
258
259 self.set_resizable(False)
260
261 #cleaning out the version variable and also the summary
262 version = version.split(":")[1]
263 if len(version) > 30:
264 version = version.split("+")[0]
265 else:
266 version = version.split("-")[0]
267 license = license.replace("&" , "and")
268 if (homepage == ''):
269 homepage = 'unknown'
270 if (bugtracker == ''):
271 bugtracker = 'unknown'
272 summary = summary.split("+")[0]
273
274 #calculating the rows needed for the table
275 binb_items_count = len(binb.split(','))
276 binb_items = binb.split(',')
277
278 vbox = gtk.VBox(False,spacing = 0)
279
280 ######################################## SUMMARY LABEL #########################################
281
282 if summary != '':
283 self.label_short = gtk.Label()
284 self.label_short.set_width_chars(37)
285 self.label_short.set_selectable(True)
286 self.label_short.set_line_wrap(True)
287 self.label_short.set_markup("<b>" + summary + "</b>")
288 self.label_short.set_property("xalign", 0)
289
290 self.vbox.add(self.label_short)
291
292 ########################################## NAME ROW + COL #######################################
293
294 self.label_short = gtk.Label()
295 self.label_short.set_selectable(True)
296 self.label_short.set_line_wrap(True)
297 self.label_short.set_markup("<span weight=\"bold\">Name: </span>" + name)
298 self.label_short.set_property("xalign", 0)
299
300 self.vbox.add(self.label_short)
301
302 ####################################### VERSION ROW + COL ####################################
303
304 self.label_short = gtk.Label()
305 self.label_short.set_selectable(True)
306 self.label_short.set_line_wrap(True)
307 self.label_short.set_markup("<span weight=\"bold\">Version: </span>" + version)
308 self.label_short.set_property("xalign", 0)
309
310 self.vbox.add(self.label_short)
311
312 ##################################### REVISION ROW + COL #####################################
313
314 self.label_short = gtk.Label()
315 self.label_short.set_line_wrap(True)
316 self.label_short.set_selectable(True)
317 self.label_short.set_markup("<span weight=\"bold\">Revision: </span>" + revision)
318 self.label_short.set_property("xalign", 0)
319
320 self.vbox.add(self.label_short)
321
322 ################################## GROUP ROW + COL ############################################
323
324 self.label_short = gtk.Label()
325 self.label_short.set_selectable(True)
326 self.label_short.set_line_wrap(True)
327 self.label_short.set_markup("<span weight=\"bold\">Group: </span>" + group)
328 self.label_short.set_property("xalign", 0)
329
330 self.vbox.add(self.label_short)
331
332 ################################# HOMEPAGE ROW + COL ############################################
333
334 if homepage != 'unknown':
335 self.label_info = gtk.Label()
336 self.label_info.set_selectable(True)
337 self.label_info.set_line_wrap(True)
338 if len(homepage) > 35:
339 self.label_info.set_markup("<a href=\"" + homepage + "\">" + homepage[0:35] + "..." + "</a>")
340 else:
341 self.label_info.set_markup("<a href=\"" + homepage + "\">" + homepage[0:60] + "</a>")
342
343 self.label_info.set_property("xalign", 0)
344
345 self.label_short = gtk.Label()
346 self.label_short.set_selectable(True)
347 self.label_short.set_line_wrap(True)
348 self.label_short.set_markup("<b>Homepage: </b>")
349 self.label_short.set_property("xalign", 0)
350
351 self.vbox.add(self.label_short)
352 self.vbox.add(self.label_info)
353
354 ################################# BUGTRACKER ROW + COL ###########################################
355
356 if bugtracker != 'unknown':
357 self.label_info = gtk.Label()
358 self.label_info.set_selectable(True)
359 self.label_info.set_line_wrap(True)
360 if len(bugtracker) > 35:
361 self.label_info.set_markup("<a href=\"" + bugtracker + "\">" + bugtracker[0:35] + "..." + "</a>")
362 else:
363 self.label_info.set_markup("<a href=\"" + bugtracker + "\">" + bugtracker[0:60] + "</a>")
364 self.label_info.set_property("xalign", 0)
365
366 self.label_short = gtk.Label()
367 self.label_short.set_selectable(True)
368 self.label_short.set_line_wrap(True)
369 self.label_short.set_markup("<b>Bugtracker: </b>")
370 self.label_short.set_property("xalign", 0)
371
372 self.vbox.add(self.label_short)
373 self.vbox.add(self.label_info)
374
375 ################################# LICENSE ROW + COL ############################################
376
377 self.label_info = gtk.Label()
378 self.label_info.set_selectable(True)
379 self.label_info.set_line_wrap(True)
380 self.label_info.set_markup(license)
381 self.label_info.set_property("xalign", 0)
382
383 self.label_short = gtk.Label()
384 self.label_short.set_selectable(True)
385 self.label_short.set_line_wrap(True)
386 self.label_short.set_markup("<span weight=\"bold\">License: </span>")
387 self.label_short.set_property("xalign", 0)
388
389 self.vbox.add(self.label_short)
390 self.vbox.add(self.label_info)
391
392 ################################### BINB ROW+COL #############################################
393
394 if binb != '':
395 self.label_short = gtk.Label()
396 self.label_short.set_selectable(True)
397 self.label_short.set_line_wrap(True)
398 self.label_short.set_markup("<span weight=\"bold\">Brought in by: </span>")
399 self.label_short.set_property("xalign", 0)
400 self.vbox.add(self.label_short)
401 self.label_info = gtk.Label()
402 self.label_info.set_selectable(True)
403 self.label_info.set_width_chars(36)
404 if len(binb) > 200:
405 scrolled_window = gtk.ScrolledWindow()
406 scrolled_window.set_policy(gtk.POLICY_NEVER,gtk.POLICY_ALWAYS)
407 scrolled_window.set_size_request(100,100)
408 self.label_info.set_markup(binb)
409 self.label_info.set_padding(6,6)
410 self.label_info.set_alignment(0,0)
411 self.label_info.set_line_wrap(True)
412 scrolled_window.add_with_viewport(self.label_info)
413 self.vbox.add(scrolled_window)
414 else:
415 self.label_info.set_markup(binb)
416 self.label_info.set_property("xalign", 0)
417 self.label_info.set_line_wrap(True)
418 self.vbox.add(self.label_info)
419
420 ################################ DESCRIPTION TAG ROW #################################################
421
422 self.label_short = gtk.Label()
423 self.label_short.set_line_wrap(True)
424 self.label_short.set_markup("<span weight=\"bold\">Description </span>")
425 self.label_short.set_property("xalign", 0)
426 self.vbox.add(self.label_short)
427
428 ################################ DESCRIPTION INFORMATION ROW ##########################################
429
430 hbox = gtk.HBox(True,spacing = 0)
431
432 self.label_short = gtk.Label()
433 self.label_short.set_selectable(True)
434 self.label_short.set_width_chars(36)
435 if len(description) > 200:
436 scrolled_window = gtk.ScrolledWindow()
437 scrolled_window.set_policy(gtk.POLICY_NEVER,gtk.POLICY_ALWAYS)
438 scrolled_window.set_size_request(100,100)
439 self.label_short.set_markup(description)
440 self.label_short.set_padding(6,6)
441 self.label_short.set_alignment(0,0)
442 self.label_short.set_line_wrap(True)
443 scrolled_window.add_with_viewport(self.label_short)
444 self.vbox.add(scrolled_window)
445 else:
446 self.label_short.set_markup(description)
447 self.label_short.set_property("xalign", 0)
448 self.label_short.set_line_wrap(True)
449 self.vbox.add(self.label_short)
450
451 self.vbox.show_all()
diff --git a/bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py
new file mode 100644
index 0000000000..69e7dffb6d
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py
@@ -0,0 +1,90 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
25
26"""
27The following are convenience classes for implementing GNOME HIG compliant
28BitBake GUI's
29In summary: spacing = 12px, border-width = 6px
30"""
31
32class ProxyDetailsDialog (CrumbsDialog):
33
34 def __init__(self, title, user, passwd, parent, flags, buttons=None):
35 super(ProxyDetailsDialog, self).__init__(title, parent, flags, buttons)
36 self.connect("response", self.response_cb)
37
38 self.auth = not (user == None or passwd == None or user == "")
39 self.user = user or ""
40 self.passwd = passwd or ""
41
42 # create visual elements on the dialog
43 self.create_visual_elements()
44
45 def create_visual_elements(self):
46 self.auth_checkbox = gtk.CheckButton("Use authentication")
47 self.auth_checkbox.set_tooltip_text("Check this box to set the username and the password")
48 self.auth_checkbox.set_active(self.auth)
49 self.auth_checkbox.connect("toggled", self.auth_checkbox_toggled_cb)
50 self.vbox.pack_start(self.auth_checkbox, expand=False, fill=False)
51
52 hbox = gtk.HBox(False, 6)
53 self.user_label = gtk.Label("Username:")
54 self.user_text = gtk.Entry()
55 self.user_text.set_text(self.user)
56 hbox.pack_start(self.user_label, expand=False, fill=False)
57 hbox.pack_end(self.user_text, expand=False, fill=False)
58 self.vbox.pack_start(hbox, expand=False, fill=False)
59
60 hbox = gtk.HBox(False, 6)
61 self.passwd_label = gtk.Label("Password:")
62 self.passwd_text = gtk.Entry()
63 self.passwd_text.set_text(self.passwd)
64 hbox.pack_start(self.passwd_label, expand=False, fill=False)
65 hbox.pack_end(self.passwd_text, expand=False, fill=False)
66 self.vbox.pack_start(hbox, expand=False, fill=False)
67
68 self.refresh_auth_components()
69 self.show_all()
70
71 def refresh_auth_components(self):
72 self.user_label.set_sensitive(self.auth)
73 self.user_text.set_editable(self.auth)
74 self.user_text.set_sensitive(self.auth)
75 self.passwd_label.set_sensitive(self.auth)
76 self.passwd_text.set_editable(self.auth)
77 self.passwd_text.set_sensitive(self.auth)
78
79 def auth_checkbox_toggled_cb(self, button):
80 self.auth = self.auth_checkbox.get_active()
81 self.refresh_auth_components()
82
83 def response_cb(self, dialog, response_id):
84 if response_id == gtk.RESPONSE_OK:
85 if self.auth:
86 self.user = self.user_text.get_text()
87 self.passwd = self.passwd_text.get_text()
88 else:
89 self.user = None
90 self.passwd = None
diff --git a/bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py
new file mode 100644
index 0000000000..9017139850
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py
@@ -0,0 +1,51 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2013 Intel Corporation
5#
6# Authored by Cristiana Voicu <cristiana.voicu@intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import gtk
22
23class RetrieveImageDialog (gtk.FileChooserDialog):
24 """
25 This class is used to create a dialog that permits to retrieve
26 a custom image saved previously from Hob.
27 """
28 def __init__(self, directory,title, parent, flags, buttons=None):
29 super(RetrieveImageDialog, self).__init__(title, None, gtk.FILE_CHOOSER_ACTION_OPEN,
30 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN, gtk.RESPONSE_OK))
31 self.directory = directory
32
33 # create visual elements on the dialog
34 self.create_visual_elements()
35
36 def create_visual_elements(self):
37 self.set_show_hidden(True)
38 self.set_default_response(gtk.RESPONSE_OK)
39 self.set_current_folder(self.directory)
40
41 vbox = self.get_children()[0].get_children()[0].get_children()[0]
42 for child in vbox.get_children()[0].get_children()[0].get_children()[0].get_children():
43 vbox.get_children()[0].get_children()[0].get_children()[0].remove(child)
44
45 label1 = gtk.Label()
46 label1.set_text("File system" + self.directory)
47 label1.show()
48 vbox.get_children()[0].get_children()[0].get_children()[0].pack_start(label1, expand=False, fill=False, padding=0)
49 vbox.get_children()[0].get_children()[1].get_children()[0].hide()
50
51 self.get_children()[0].get_children()[1].get_children()[0].set_label("Select")
diff --git a/bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py
new file mode 100644
index 0000000000..e940ceee43
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py
@@ -0,0 +1,160 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2013 Intel Corporation
5#
6# Authored by Cristiana Voicu <cristiana.voicu@intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import gtk
22import glib
23from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
24from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog
25from bb.ui.crumbs.hobwidget import HobButton
26
27class SaveImageDialog (CrumbsDialog):
28 """
29 This class is used to create a dialog that permits to save
30 a custom image in a predefined directory.
31 """
32 def __init__(self, directory, name, description, title, parent, flags, buttons=None):
33 super(SaveImageDialog, self).__init__(title, parent, flags, buttons)
34 self.directory = directory
35 self.builder = parent
36 self.name_field = name
37 self.description_field = description
38
39 # create visual elements on the dialog
40 self.create_visual_elements()
41
42 def create_visual_elements(self):
43 self.set_default_response(gtk.RESPONSE_OK)
44 self.vbox.set_border_width(6)
45
46 sub_vbox = gtk.VBox(False, 12)
47 self.vbox.pack_start(sub_vbox, expand=False, fill=False)
48 label = gtk.Label()
49 label.set_alignment(0, 0)
50 label.set_markup("<b>Name</b>")
51 sub_label = gtk.Label()
52 sub_label.set_alignment(0, 0)
53 content = "Image recipe names should be all lowercase and include only alphanumeric\n"
54 content += "characters. The only special character you can use is the ASCII hyphen (-)."
55 sub_label.set_markup(content)
56 self.name_entry = gtk.Entry()
57 self.name_entry.set_text(self.name_field)
58 self.name_entry.set_size_request(350,30)
59 self.name_entry.connect("changed", self.name_entry_changed)
60 sub_vbox.pack_start(label, expand=False, fill=False)
61 sub_vbox.pack_start(sub_label, expand=False, fill=False)
62 sub_vbox.pack_start(self.name_entry, expand=False, fill=False)
63
64 sub_vbox = gtk.VBox(False, 12)
65 self.vbox.pack_start(sub_vbox, expand=False, fill=False)
66 label = gtk.Label()
67 label.set_alignment(0, 0)
68 label.set_markup("<b>Description</b> (optional)")
69 sub_label = gtk.Label()
70 sub_label.set_alignment(0, 0)
71 sub_label.set_markup("The description should be less than 150 characters long.")
72 self.description_entry = gtk.TextView()
73 description_buffer = self.description_entry.get_buffer()
74 description_buffer.set_text(self.description_field)
75 description_buffer.connect("insert-text", self.limit_description_length)
76 self.description_entry.set_wrap_mode(gtk.WRAP_WORD)
77 self.description_entry.set_size_request(350,50)
78 sub_vbox.pack_start(label, expand=False, fill=False)
79 sub_vbox.pack_start(sub_label, expand=False, fill=False)
80 sub_vbox.pack_start(self.description_entry, expand=False, fill=False)
81
82 sub_vbox = gtk.VBox(False, 12)
83 self.vbox.pack_start(sub_vbox, expand=False, fill=False)
84 label = gtk.Label()
85 label.set_alignment(0, 0)
86 label.set_markup("Your image recipe will be saved to:")
87 sub_label = gtk.Label()
88 sub_label.set_alignment(0, 0)
89 sub_label.set_markup(self.directory)
90 sub_vbox.pack_start(label, expand=False, fill=False)
91 sub_vbox.pack_start(sub_label, expand=False, fill=False)
92
93 table = gtk.Table(1, 4, True)
94
95 cancel_button = gtk.Button()
96 cancel_button.set_label("Cancel")
97 cancel_button.connect("clicked", self.cancel_button_cb)
98 cancel_button.set_size_request(110, 30)
99
100 self.save_button = gtk.Button()
101 self.save_button.set_label("Save")
102 self.save_button.connect("clicked", self.save_button_cb)
103 self.save_button.set_size_request(110, 30)
104 if self.name_entry.get_text() == '':
105 self.save_button.set_sensitive(False)
106
107 table.attach(cancel_button, 2, 3, 0, 1)
108 table.attach(self.save_button, 3, 4, 0, 1)
109 self.vbox.pack_end(table, expand=False, fill=False)
110
111 self.show_all()
112
113 def limit_description_length(self, textbuffer, iter, text, length):
114 buffer_bounds = textbuffer.get_bounds()
115 entire_text = textbuffer.get_text(*buffer_bounds)
116 entire_text += text
117 if len(entire_text)>150 or text=="\n":
118 textbuffer.emit_stop_by_name("insert-text")
119
120 def name_entry_changed(self, entry):
121 text = entry.get_text()
122 if text == '':
123 self.save_button.set_sensitive(False)
124 else:
125 self.save_button.set_sensitive(True)
126
127 def cancel_button_cb(self, button):
128 self.destroy()
129
130 def save_button_cb(self, button):
131 text = self.name_entry.get_text()
132 new_text = text.replace("-","")
133 description_buffer = self.description_entry.get_buffer()
134 description = description_buffer.get_text(description_buffer.get_start_iter(),description_buffer.get_end_iter())
135 if new_text.islower() and new_text.isalnum():
136 self.builder.image_details_page.image_saved = True
137 self.builder.customized = False
138 self.builder.generate_new_image(self.directory+text, description)
139 self.builder.recipe_model.set_in_list(text, description)
140 self.builder.recipe_model.set_selected_image(text)
141 self.builder.image_details_page.show_page(self.builder.IMAGE_GENERATED)
142 self.builder.image_details_page.name_field_template = text
143 self.builder.image_details_page.description_field_template = description
144 self.destroy()
145 else:
146 self.show_invalid_input_error_dialog()
147
148 def show_invalid_input_error_dialog(self):
149 lbl = "<b>Invalid characters in image recipe name</b>\n"
150 msg = "Image recipe names should be all lowercase and\n"
151 msg += "include only alphanumeric characters. The only\n"
152 msg += "special character you can use is the ASCII hyphen (-)."
153 lbl = lbl + "\n%s\n" % glib.markup_escape_text(msg)
154 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_ERROR)
155 button = dialog.add_button("Close", gtk.RESPONSE_OK)
156 HobButton.style_button(button)
157
158 res = dialog.run()
159 self.name_entry.grab_focus()
160 dialog.destroy()
diff --git a/bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py b/bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py
new file mode 100644
index 0000000000..e0285c93ce
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py
@@ -0,0 +1,122 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import os
25from bb.ui.crumbs.hobwidget import HobInfoButton, HobButton, HobAltButton
26
27"""
28The following are convenience classes for implementing GNOME HIG compliant
29BitBake GUI's
30In summary: spacing = 12px, border-width = 6px
31"""
32
33class SettingsUIHelper():
34
35 def gen_label_widget(self, content):
36 label = gtk.Label()
37 label.set_alignment(0, 0)
38 label.set_markup(content)
39 label.show()
40 return label
41
42 def gen_label_info_widget(self, content, tooltip):
43 table = gtk.Table(1, 10, False)
44 label = self.gen_label_widget(content)
45 info = HobInfoButton(tooltip, self)
46 table.attach(label, 0, 1, 0, 1, xoptions=gtk.FILL)
47 table.attach(info, 1, 2, 0, 1, xoptions=gtk.FILL, xpadding=10)
48 return table
49
50 def gen_spinner_widget(self, content, lower, upper, tooltip=""):
51 hbox = gtk.HBox(False, 12)
52 adjust = gtk.Adjustment(value=content, lower=lower, upper=upper, step_incr=1)
53 spinner = gtk.SpinButton(adjustment=adjust, climb_rate=1, digits=0)
54
55 spinner.set_value(content)
56 hbox.pack_start(spinner, expand=False, fill=False)
57
58 info = HobInfoButton(tooltip, self)
59 hbox.pack_start(info, expand=False, fill=False)
60
61 hbox.show_all()
62 return hbox, spinner
63
64 def gen_combo_widget(self, curr_item, all_item, tooltip=""):
65 hbox = gtk.HBox(False, 12)
66 combo = gtk.combo_box_new_text()
67 hbox.pack_start(combo, expand=False, fill=False)
68
69 index = 0
70 for item in all_item or []:
71 combo.append_text(item)
72 if item == curr_item:
73 combo.set_active(index)
74 index += 1
75
76 info = HobInfoButton(tooltip, self)
77 hbox.pack_start(info, expand=False, fill=False)
78
79 hbox.show_all()
80 return hbox, combo
81
82 def entry_widget_select_path_cb(self, action, parent, entry):
83 dialog = gtk.FileChooserDialog("", parent,
84 gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
85 text = entry.get_text()
86 dialog.set_current_folder(text if len(text) > 0 else os.getcwd())
87 button = dialog.add_button("Cancel", gtk.RESPONSE_NO)
88 HobAltButton.style_button(button)
89 button = dialog.add_button("Open", gtk.RESPONSE_YES)
90 HobButton.style_button(button)
91 response = dialog.run()
92 if response == gtk.RESPONSE_YES:
93 path = dialog.get_filename()
94 entry.set_text(path)
95
96 dialog.destroy()
97
98 def gen_entry_widget(self, content, parent, tooltip="", need_button=True):
99 hbox = gtk.HBox(False, 12)
100 entry = gtk.Entry()
101 entry.set_text(content)
102 entry.set_size_request(350,30)
103
104 if need_button:
105 table = gtk.Table(1, 10, False)
106 hbox.pack_start(table, expand=True, fill=True)
107 table.attach(entry, 0, 9, 0, 1, xoptions=gtk.SHRINK)
108 image = gtk.Image()
109 image.set_from_stock(gtk.STOCK_OPEN,gtk.ICON_SIZE_BUTTON)
110 open_button = gtk.Button()
111 open_button.set_image(image)
112 open_button.connect("clicked", self.entry_widget_select_path_cb, parent, entry)
113 table.attach(open_button, 9, 10, 0, 1, xoptions=gtk.SHRINK)
114 else:
115 hbox.pack_start(entry, expand=True, fill=True)
116
117 if tooltip != "":
118 info = HobInfoButton(tooltip, self)
119 hbox.pack_start(info, expand=False, fill=False)
120
121 hbox.show_all()
122 return hbox, entry
diff --git a/bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py
new file mode 100644
index 0000000000..de924b1206
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py
@@ -0,0 +1,893 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011-2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import gobject
25import hashlib
26from bb.ui.crumbs.hobwidget import hic, HobInfoButton, HobButton, HobAltButton
27from bb.ui.crumbs.progressbar import HobProgressBar
28from bb.ui.crumbs.hig.settingsuihelper import SettingsUIHelper
29from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
30from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog
31from bb.ui.crumbs.hig.proxydetailsdialog import ProxyDetailsDialog
32
33"""
34The following are convenience classes for implementing GNOME HIG compliant
35BitBake GUI's
36In summary: spacing = 12px, border-width = 6px
37"""
38
39class SimpleSettingsDialog (CrumbsDialog, SettingsUIHelper):
40
41 (BUILD_ENV_PAGE_ID,
42 SHARED_STATE_PAGE_ID,
43 PROXIES_PAGE_ID,
44 OTHERS_PAGE_ID) = range(4)
45
46 (TEST_NETWORK_NONE,
47 TEST_NETWORK_INITIAL,
48 TEST_NETWORK_RUNNING,
49 TEST_NETWORK_PASSED,
50 TEST_NETWORK_FAILED,
51 TEST_NETWORK_CANCELED) = range(6)
52
53 TARGETS = [
54 ("MY_TREE_MODEL_ROW", gtk.TARGET_SAME_WIDGET, 0),
55 ("text/plain", 0, 1),
56 ("TEXT", 0, 2),
57 ("STRING", 0, 3),
58 ]
59
60 def __init__(self, title, configuration, all_image_types,
61 all_package_formats, all_distros, all_sdk_machines,
62 max_threads, parent, flags, handler, buttons=None):
63 super(SimpleSettingsDialog, self).__init__(title, parent, flags, buttons)
64
65 # class members from other objects
66 # bitbake settings from Builder.Configuration
67 self.configuration = configuration
68 self.image_types = all_image_types
69 self.all_package_formats = all_package_formats
70 self.all_distros = all_distros
71 self.all_sdk_machines = all_sdk_machines
72 self.max_threads = max_threads
73
74 # class members for internal use
75 self.dldir_text = None
76 self.sstatedir_text = None
77 self.sstatemirrors_list = []
78 self.sstatemirrors_changed = 0
79 self.bb_spinner = None
80 self.pmake_spinner = None
81 self.rootfs_size_spinner = None
82 self.extra_size_spinner = None
83 self.gplv3_checkbox = None
84 self.toolchain_checkbox = None
85 self.setting_store = None
86 self.image_types_checkbuttons = {}
87
88 self.md5 = self.config_md5()
89 self.proxy_md5 = self.config_proxy_md5()
90 self.settings_changed = False
91 self.proxy_settings_changed = False
92 self.handler = handler
93 self.proxy_test_ran = False
94 self.selected_mirror_row = 0
95 self.new_mirror = False
96
97 # create visual elements on the dialog
98 self.create_visual_elements()
99 self.connect("response", self.response_cb)
100
101 def _get_sorted_value(self, var):
102 return " ".join(sorted(str(var).split())) + "\n"
103
104 def config_proxy_md5(self):
105 data = ("ENABLE_PROXY: " + self._get_sorted_value(self.configuration.enable_proxy))
106 if self.configuration.enable_proxy:
107 for protocol in self.configuration.proxies.keys():
108 data += (protocol + ": " + self._get_sorted_value(self.configuration.combine_proxy(protocol)))
109 return hashlib.md5(data).hexdigest()
110
111 def config_md5(self):
112 data = ""
113 for key in self.configuration.extra_setting.keys():
114 data += (key + ": " + self._get_sorted_value(self.configuration.extra_setting[key]))
115 return hashlib.md5(data).hexdigest()
116
117 def gen_proxy_entry_widget(self, protocol, parent, need_button=True, line=0):
118 label = gtk.Label(protocol.upper() + " proxy")
119 self.proxy_table.attach(label, 0, 1, line, line+1, xpadding=24)
120
121 proxy_entry = gtk.Entry()
122 proxy_entry.set_size_request(300, -1)
123 self.proxy_table.attach(proxy_entry, 1, 2, line, line+1, ypadding=4)
124
125 self.proxy_table.attach(gtk.Label(":"), 2, 3, line, line+1, xpadding=12, ypadding=4)
126
127 port_entry = gtk.Entry()
128 port_entry.set_size_request(60, -1)
129 self.proxy_table.attach(port_entry, 3, 4, line, line+1, ypadding=4)
130
131 details_button = HobAltButton("Details")
132 details_button.connect("clicked", self.details_cb, parent, protocol)
133 self.proxy_table.attach(details_button, 4, 5, line, line+1, xpadding=4, yoptions=gtk.EXPAND)
134
135 return proxy_entry, port_entry, details_button
136
137 def refresh_proxy_components(self):
138 self.same_checkbox.set_sensitive(self.configuration.enable_proxy)
139
140 self.http_proxy.set_text(self.configuration.combine_host_only("http"))
141 self.http_proxy.set_editable(self.configuration.enable_proxy)
142 self.http_proxy.set_sensitive(self.configuration.enable_proxy)
143 self.http_proxy_port.set_text(self.configuration.combine_port_only("http"))
144 self.http_proxy_port.set_editable(self.configuration.enable_proxy)
145 self.http_proxy_port.set_sensitive(self.configuration.enable_proxy)
146 self.http_proxy_details.set_sensitive(self.configuration.enable_proxy)
147
148 self.https_proxy.set_text(self.configuration.combine_host_only("https"))
149 self.https_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
150 self.https_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
151 self.https_proxy_port.set_text(self.configuration.combine_port_only("https"))
152 self.https_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
153 self.https_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
154 self.https_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
155
156 self.ftp_proxy.set_text(self.configuration.combine_host_only("ftp"))
157 self.ftp_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
158 self.ftp_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
159 self.ftp_proxy_port.set_text(self.configuration.combine_port_only("ftp"))
160 self.ftp_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
161 self.ftp_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
162 self.ftp_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
163
164 self.socks_proxy.set_text(self.configuration.combine_host_only("socks"))
165 self.socks_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
166 self.socks_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
167 self.socks_proxy_port.set_text(self.configuration.combine_port_only("socks"))
168 self.socks_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
169 self.socks_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
170 self.socks_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
171
172 self.cvs_proxy.set_text(self.configuration.combine_host_only("cvs"))
173 self.cvs_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
174 self.cvs_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
175 self.cvs_proxy_port.set_text(self.configuration.combine_port_only("cvs"))
176 self.cvs_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy))
177 self.cvs_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
178 self.cvs_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy))
179
180 if self.configuration.same_proxy:
181 if self.http_proxy.get_text():
182 [w.set_text(self.http_proxy.get_text()) for w in self.same_proxy_addresses]
183 if self.http_proxy_port.get_text():
184 [w.set_text(self.http_proxy_port.get_text()) for w in self.same_proxy_ports]
185
186 def proxy_checkbox_toggled_cb(self, button):
187 self.configuration.enable_proxy = self.proxy_checkbox.get_active()
188 if not self.configuration.enable_proxy:
189 self.configuration.same_proxy = False
190 self.same_checkbox.set_active(self.configuration.same_proxy)
191 self.save_proxy_data()
192 self.refresh_proxy_components()
193
194 def same_checkbox_toggled_cb(self, button):
195 self.configuration.same_proxy = self.same_checkbox.get_active()
196 self.save_proxy_data()
197 self.refresh_proxy_components()
198
199 def save_proxy_data(self):
200 self.configuration.split_proxy("http", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text())
201 if self.configuration.same_proxy:
202 self.configuration.split_proxy("https", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text())
203 self.configuration.split_proxy("ftp", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text())
204 self.configuration.split_proxy("socks", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text())
205 self.configuration.split_proxy("cvs", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text())
206 else:
207 self.configuration.split_proxy("https", self.https_proxy.get_text() + ":" + self.https_proxy_port.get_text())
208 self.configuration.split_proxy("ftp", self.ftp_proxy.get_text() + ":" + self.ftp_proxy_port.get_text())
209 self.configuration.split_proxy("socks", self.socks_proxy.get_text() + ":" + self.socks_proxy_port.get_text())
210 self.configuration.split_proxy("cvs", self.cvs_proxy.get_text() + ":" + self.cvs_proxy_port.get_text())
211
212 def response_cb(self, dialog, response_id):
213 if response_id == gtk.RESPONSE_YES:
214 # Check that all proxy entries have a corresponding port
215 for proxy, port in zip(self.all_proxy_addresses, self.all_proxy_ports):
216 if proxy.get_text() and not port.get_text():
217 lbl = "<b>Enter all port numbers</b>\n\n"
218 msg = "Proxy servers require a port number. Please make sure you have entered a port number for each proxy server."
219 dialog = CrumbsMessageDialog(self, lbl, gtk.STOCK_DIALOG_WARNING, msg)
220 button = dialog.add_button("Close", gtk.RESPONSE_OK)
221 HobButton.style_button(button)
222 response = dialog.run()
223 dialog.destroy()
224 self.emit_stop_by_name("response")
225 return
226
227 self.configuration.dldir = self.dldir_text.get_text()
228 self.configuration.sstatedir = self.sstatedir_text.get_text()
229 self.configuration.sstatemirror = ""
230 for mirror in self.sstatemirrors_list:
231 if mirror[1] != "" and mirror[2].startswith("file://"):
232 if mirror[1].endswith("\\1"):
233 smirror = mirror[2] + " " + mirror[1] + " \\n "
234 else:
235 smirror = mirror[2] + " " + mirror[1] + "\\1 \\n "
236 self.configuration.sstatemirror += smirror
237 self.configuration.bbthread = self.bb_spinner.get_value_as_int()
238 self.configuration.pmake = self.pmake_spinner.get_value_as_int()
239 self.save_proxy_data()
240 self.configuration.extra_setting = {}
241 it = self.setting_store.get_iter_first()
242 while it:
243 key = self.setting_store.get_value(it, 0)
244 value = self.setting_store.get_value(it, 1)
245 self.configuration.extra_setting[key] = value
246 it = self.setting_store.iter_next(it)
247
248 md5 = self.config_md5()
249 self.settings_changed = (self.md5 != md5)
250 self.proxy_settings_changed = (self.proxy_md5 != self.config_proxy_md5())
251
252 def create_build_environment_page(self):
253 advanced_vbox = gtk.VBox(False, 6)
254 advanced_vbox.set_border_width(6)
255
256 advanced_vbox.pack_start(self.gen_label_widget('<span weight="bold">Parallel threads</span>'), expand=False, fill=False)
257 sub_vbox = gtk.VBox(False, 6)
258 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False)
259 label = self.gen_label_widget("BitBake parallel threads")
260 tooltip = "Sets the number of threads that BitBake tasks can simultaneously run. See the <a href=\""
261 tooltip += "http://www.yoctoproject.org/docs/current/poky-ref-manual/"
262 tooltip += "poky-ref-manual.html#var-BB_NUMBER_THREADS\">Poky reference manual</a> for information"
263 bbthread_widget, self.bb_spinner = self.gen_spinner_widget(self.configuration.bbthread, 1, self.max_threads,"<b>BitBake prallalel threads</b>" + "*" + tooltip)
264 sub_vbox.pack_start(label, expand=False, fill=False)
265 sub_vbox.pack_start(bbthread_widget, expand=False, fill=False)
266
267 sub_vbox = gtk.VBox(False, 6)
268 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False)
269 label = self.gen_label_widget("Make parallel threads")
270 tooltip = "Sets the maximum number of threads the host can use during the build. See the <a href=\""
271 tooltip += "http://www.yoctoproject.org/docs/current/poky-ref-manual/"
272 tooltip += "poky-ref-manual.html#var-PARALLEL_MAKE\">Poky reference manual</a> for information"
273 pmake_widget, self.pmake_spinner = self.gen_spinner_widget(self.configuration.pmake, 1, self.max_threads,"<b>Make parallel threads</b>" + "*" + tooltip)
274 sub_vbox.pack_start(label, expand=False, fill=False)
275 sub_vbox.pack_start(pmake_widget, expand=False, fill=False)
276
277 advanced_vbox.pack_start(self.gen_label_widget('<span weight="bold">Downloaded source code</span>'), expand=False, fill=False)
278 sub_vbox = gtk.VBox(False, 6)
279 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False)
280 label = self.gen_label_widget("Downloads directory")
281 tooltip = "Select a folder that caches the upstream project source code"
282 dldir_widget, self.dldir_text = self.gen_entry_widget(self.configuration.dldir, self,"<b>Downloaded source code</b>" + "*" + tooltip)
283 sub_vbox.pack_start(label, expand=False, fill=False)
284 sub_vbox.pack_start(dldir_widget, expand=False, fill=False)
285
286 return advanced_vbox
287
288 def create_shared_state_page(self):
289 advanced_vbox = gtk.VBox(False)
290 advanced_vbox.set_border_width(12)
291
292 sub_vbox = gtk.VBox(False)
293 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False, padding=24)
294 content = "<span>Shared state directory</span>"
295 tooltip = "Select a folder that caches your prebuilt results"
296 label = self.gen_label_info_widget(content,"<b>Shared state directory</b>" + "*" + tooltip)
297 sstatedir_widget, self.sstatedir_text = self.gen_entry_widget(self.configuration.sstatedir, self)
298 sub_vbox.pack_start(label, expand=False, fill=False)
299 sub_vbox.pack_start(sstatedir_widget, expand=False, fill=False, padding=6)
300
301 content = "<span weight=\"bold\">Shared state mirrors</span>"
302 tooltip = "URLs pointing to pre-built mirrors that will speed your build. "
303 tooltip += "Select the \'Standard\' configuration if the structure of your "
304 tooltip += "mirror replicates the structure of your local shared state directory. "
305 tooltip += "For more information on shared state mirrors, check the <a href=\""
306 tooltip += "http://www.yoctoproject.org/docs/current/poky-ref-manual/"
307 tooltip += "poky-ref-manual.html#shared-state\">Yocto Project Reference Manual</a>."
308 table = self.gen_label_info_widget(content,"<b>Shared state mirrors</b>" + "*" + tooltip)
309 advanced_vbox.pack_start(table, expand=False, fill=False, padding=6)
310
311 sub_vbox = gtk.VBox(False)
312 advanced_vbox.pack_start(sub_vbox, gtk.TRUE, gtk.TRUE, 0)
313
314 if self.sstatemirrors_changed == 0:
315 self.sstatemirrors_changed = 1
316 sstatemirrors = self.configuration.sstatemirror
317 if sstatemirrors == "":
318 sm_list = ["Standard", "", "file://(.*)"]
319 self.sstatemirrors_list.append(sm_list)
320 else:
321 sstatemirrors = [x for x in sstatemirrors.split('\\n')]
322 for sstatemirror in sstatemirrors:
323 sstatemirror_fields = [x for x in sstatemirror.split(' ') if x.strip()]
324 if len(sstatemirror_fields) == 2:
325 if sstatemirror_fields[0] == "file://(.*)" or sstatemirror_fields[0] == "file://.*":
326 sm_list = ["Standard", sstatemirror_fields[1], sstatemirror_fields[0]]
327 else:
328 sm_list = ["Custom", sstatemirror_fields[1], sstatemirror_fields[0]]
329 self.sstatemirrors_list.append(sm_list)
330
331 sstatemirrors_widget, sstatemirrors_store = self.gen_shared_sstate_widget(self.sstatemirrors_list, self)
332 sub_vbox.pack_start(sstatemirrors_widget, expand=True, fill=True)
333
334 table = gtk.Table(1, 10, False)
335 table.set_col_spacings(6)
336 add_mirror_button = HobAltButton("Add mirror")
337 add_mirror_button.connect("clicked", self.add_mirror)
338 add_mirror_button.set_size_request(120,30)
339 table.attach(add_mirror_button, 1, 2, 0, 1, xoptions=gtk.SHRINK)
340
341 self.delete_button = HobAltButton("Delete mirror")
342 self.delete_button.connect("clicked", self.delete_cb)
343 self.delete_button.set_size_request(120, 30)
344 table.attach(self.delete_button, 3, 4, 0, 1, xoptions=gtk.SHRINK)
345
346 advanced_vbox.pack_start(table, expand=False, fill=False, padding=6)
347
348 return advanced_vbox
349
350 def gen_shared_sstate_widget(self, sstatemirrors_list, window):
351 hbox = gtk.HBox(False)
352
353 sstatemirrors_store = gtk.ListStore(str, str, str)
354 for sstatemirror in sstatemirrors_list:
355 sstatemirrors_store.append(sstatemirror)
356
357 self.sstatemirrors_tv = gtk.TreeView()
358 self.sstatemirrors_tv.set_rules_hint(True)
359 self.sstatemirrors_tv.set_headers_visible(True)
360 tree_selection = self.sstatemirrors_tv.get_selection()
361 tree_selection.set_mode(gtk.SELECTION_SINGLE)
362
363 # Allow enable drag and drop of rows including row move
364 self.sstatemirrors_tv.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
365 self.TARGETS,
366 gtk.gdk.ACTION_DEFAULT|
367 gtk.gdk.ACTION_MOVE)
368 self.sstatemirrors_tv.enable_model_drag_dest(self.TARGETS,
369 gtk.gdk.ACTION_DEFAULT)
370 self.sstatemirrors_tv.connect("drag_data_get", self.drag_data_get_cb)
371 self.sstatemirrors_tv.connect("drag_data_received", self.drag_data_received_cb)
372
373
374 self.scroll = gtk.ScrolledWindow()
375 self.scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
376 self.scroll.set_shadow_type(gtk.SHADOW_IN)
377 self.scroll.connect('size-allocate', self.scroll_changed)
378 self.scroll.add(self.sstatemirrors_tv)
379
380 #list store for cell renderer
381 m = gtk.ListStore(gobject.TYPE_STRING)
382 m.append(["Standard"])
383 m.append(["Custom"])
384
385 cell0 = gtk.CellRendererCombo()
386 cell0.set_property("model",m)
387 cell0.set_property("text-column", 0)
388 cell0.set_property("editable", True)
389 cell0.set_property("has-entry", False)
390 col0 = gtk.TreeViewColumn("Configuration")
391 col0.pack_start(cell0, False)
392 col0.add_attribute(cell0, "text", 0)
393 col0.set_cell_data_func(cell0, self.configuration_field)
394 self.sstatemirrors_tv.append_column(col0)
395
396 cell0.connect("edited", self.combo_changed, sstatemirrors_store)
397
398 self.cell1 = gtk.CellRendererText()
399 self.cell1.set_padding(5,2)
400 col1 = gtk.TreeViewColumn('Regex', self.cell1)
401 col1.set_cell_data_func(self.cell1, self.regex_field)
402 self.sstatemirrors_tv.append_column(col1)
403
404 self.cell1.connect("edited", self.regex_changed, sstatemirrors_store)
405
406 cell2 = gtk.CellRendererText()
407 cell2.set_padding(5,2)
408 cell2.set_property("editable", True)
409 col2 = gtk.TreeViewColumn('URL', cell2)
410 col2.set_cell_data_func(cell2, self.url_field)
411 self.sstatemirrors_tv.append_column(col2)
412
413 cell2.connect("edited", self.url_changed, sstatemirrors_store)
414
415 self.sstatemirrors_tv.set_model(sstatemirrors_store)
416 self.sstatemirrors_tv.set_cursor(self.selected_mirror_row)
417 hbox.pack_start(self.scroll, expand=True, fill=True)
418 hbox.show_all()
419
420 return hbox, sstatemirrors_store
421
422 def drag_data_get_cb(self, treeview, context, selection, target_id, etime):
423 treeselection = treeview.get_selection()
424 model, iter = treeselection.get_selected()
425 data = model.get_string_from_iter(iter)
426 selection.set(selection.target, 8, data)
427
428 def drag_data_received_cb(self, treeview, context, x, y, selection, info, etime):
429 model = treeview.get_model()
430 data = []
431 tree_iter = model.get_iter_from_string(selection.data)
432 data.append(model.get_value(tree_iter, 0))
433 data.append(model.get_value(tree_iter, 1))
434 data.append(model.get_value(tree_iter, 2))
435
436 drop_info = treeview.get_dest_row_at_pos(x, y)
437 if drop_info:
438 path, position = drop_info
439 iter = model.get_iter(path)
440 if (position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
441 model.insert_before(iter, data)
442 else:
443 model.insert_after(iter, data)
444 else:
445 model.append(data)
446 if context.action == gtk.gdk.ACTION_MOVE:
447 context.finish(True, True, etime)
448 return
449
450 def delete_cb(self, button):
451 selection = self.sstatemirrors_tv.get_selection()
452 tree_model, tree_iter = selection.get_selected()
453 index = int(tree_model.get_string_from_iter(tree_iter))
454 if index == 0:
455 self.selected_mirror_row = index
456 else:
457 self.selected_mirror_row = index - 1
458 self.sstatemirrors_list.pop(index)
459 self.refresh_shared_state_page()
460 if not self.sstatemirrors_list:
461 self.delete_button.set_sensitive(False)
462
463 def add_mirror(self, button):
464 self.new_mirror = True
465 tooltip = "Select the pre-built mirror that will speed your build"
466 index = len(self.sstatemirrors_list)
467 self.selected_mirror_row = index
468 sm_list = ["Standard", "", "file://(.*)"]
469 self.sstatemirrors_list.append(sm_list)
470 self.refresh_shared_state_page()
471
472 def scroll_changed(self, widget, event, data=None):
473 if self.new_mirror == True:
474 adj = widget.get_vadjustment()
475 adj.set_value(adj.upper - adj.page_size)
476 self.new_mirror = False
477
478 def combo_changed(self, widget, path, text, model):
479 model[path][0] = text
480 selection = self.sstatemirrors_tv.get_selection()
481 tree_model, tree_iter = selection.get_selected()
482 index = int(tree_model.get_string_from_iter(tree_iter))
483 self.sstatemirrors_list[index][0] = text
484
485 def regex_changed(self, cell, path, new_text, user_data):
486 user_data[path][2] = new_text
487 selection = self.sstatemirrors_tv.get_selection()
488 tree_model, tree_iter = selection.get_selected()
489 index = int(tree_model.get_string_from_iter(tree_iter))
490 self.sstatemirrors_list[index][2] = new_text
491 return
492
493 def url_changed(self, cell, path, new_text, user_data):
494 if new_text!="Enter the mirror URL" and new_text!="Match regex and replace it with this URL":
495 user_data[path][1] = new_text
496 selection = self.sstatemirrors_tv.get_selection()
497 tree_model, tree_iter = selection.get_selected()
498 index = int(tree_model.get_string_from_iter(tree_iter))
499 self.sstatemirrors_list[index][1] = new_text
500 return
501
502 def configuration_field(self, column, cell, model, iter):
503 cell.set_property('text', model.get_value(iter, 0))
504 if model.get_value(iter, 0) == "Standard":
505 self.cell1.set_property("sensitive", False)
506 self.cell1.set_property("editable", False)
507 else:
508 self.cell1.set_property("sensitive", True)
509 self.cell1.set_property("editable", True)
510 return
511
512 def regex_field(self, column, cell, model, iter):
513 cell.set_property('text', model.get_value(iter, 2))
514 return
515
516 def url_field(self, column, cell, model, iter):
517 text = model.get_value(iter, 1)
518 if text == "":
519 if model.get_value(iter, 0) == "Standard":
520 text = "Enter the mirror URL"
521 else:
522 text = "Match regex and replace it with this URL"
523 cell.set_property('text', text)
524 return
525
526 def refresh_shared_state_page(self):
527 page_num = self.nb.get_current_page()
528 self.nb.remove_page(page_num);
529 self.nb.insert_page(self.create_shared_state_page(), gtk.Label("Shared state"),page_num)
530 self.show_all()
531 self.nb.set_current_page(page_num)
532
533 def test_proxy_ended(self, passed):
534 self.proxy_test_running = False
535 self.set_test_proxy_state(self.TEST_NETWORK_PASSED if passed else self.TEST_NETWORK_FAILED)
536 self.set_sensitive(True)
537 self.refresh_proxy_components()
538
539 def timer_func(self):
540 self.test_proxy_progress.pulse()
541 return self.proxy_test_running
542
543 def test_network_button_cb(self, b):
544 self.set_test_proxy_state(self.TEST_NETWORK_RUNNING)
545 self.set_sensitive(False)
546 self.save_proxy_data()
547 if self.configuration.enable_proxy == True:
548 self.handler.set_http_proxy(self.configuration.combine_proxy("http"))
549 self.handler.set_https_proxy(self.configuration.combine_proxy("https"))
550 self.handler.set_ftp_proxy(self.configuration.combine_proxy("ftp"))
551 self.handler.set_socks_proxy(self.configuration.combine_proxy("socks"))
552 self.handler.set_cvs_proxy(self.configuration.combine_host_only("cvs"), self.configuration.combine_port_only("cvs"))
553 elif self.configuration.enable_proxy == False:
554 self.handler.set_http_proxy("")
555 self.handler.set_https_proxy("")
556 self.handler.set_ftp_proxy("")
557 self.handler.set_socks_proxy("")
558 self.handler.set_cvs_proxy("", "")
559 self.proxy_test_ran = True
560 self.proxy_test_running = True
561 gobject.timeout_add(100, self.timer_func)
562 self.handler.trigger_network_test()
563
564 def test_proxy_focus_event(self, w, direction):
565 if self.test_proxy_state in [self.TEST_NETWORK_PASSED, self.TEST_NETWORK_FAILED]:
566 self.set_test_proxy_state(self.TEST_NETWORK_INITIAL)
567 return False
568
569 def http_proxy_changed(self, e):
570 if not self.configuration.same_proxy:
571 return
572 if e == self.http_proxy:
573 [w.set_text(self.http_proxy.get_text()) for w in self.same_proxy_addresses]
574 else:
575 [w.set_text(self.http_proxy_port.get_text()) for w in self.same_proxy_ports]
576
577 def proxy_address_focus_out_event(self, w, direction):
578 text = w.get_text()
579 if not text:
580 return False
581 if text.find("//") == -1:
582 w.set_text("http://" + text)
583 return False
584
585 def set_test_proxy_state(self, state):
586 if self.test_proxy_state == state:
587 return
588 [self.proxy_table.remove(w) for w in self.test_gui_elements]
589 if state == self.TEST_NETWORK_INITIAL:
590 self.proxy_table.attach(self.test_network_button, 1, 2, 5, 6)
591 self.test_network_button.show()
592 elif state == self.TEST_NETWORK_RUNNING:
593 self.test_proxy_progress.set_rcstyle("running")
594 self.test_proxy_progress.set_text("Testing network configuration")
595 self.proxy_table.attach(self.test_proxy_progress, 0, 5, 5, 6, xpadding=4)
596 self.test_proxy_progress.show()
597 else: # passed or failed
598 self.dummy_progress.update(1.0)
599 if state == self.TEST_NETWORK_PASSED:
600 self.dummy_progress.set_text("Your network is properly configured")
601 self.dummy_progress.set_rcstyle("running")
602 else:
603 self.dummy_progress.set_text("Network test failed")
604 self.dummy_progress.set_rcstyle("fail")
605 self.proxy_table.attach(self.dummy_progress, 0, 4, 5, 6)
606 self.proxy_table.attach(self.retest_network_button, 4, 5, 5, 6, xpadding=4)
607 self.dummy_progress.show()
608 self.retest_network_button.show()
609 self.test_proxy_state = state
610
611 def create_network_page(self):
612 advanced_vbox = gtk.VBox(False, 6)
613 advanced_vbox.set_border_width(6)
614 self.same_proxy_addresses = []
615 self.same_proxy_ports = []
616 self.all_proxy_ports = []
617 self.all_proxy_addresses = []
618
619 sub_vbox = gtk.VBox(False, 6)
620 advanced_vbox.pack_start(sub_vbox, expand=False, fill=False)
621 label = self.gen_label_widget("<span weight=\"bold\">Set the proxies used when fetching source code</span>")
622 tooltip = "Set the proxies used when fetching source code. A blank field uses a direct internet connection."
623 info = HobInfoButton("<span weight=\"bold\">Set the proxies used when fetching source code</span>" + "*" + tooltip, self)
624 hbox = gtk.HBox(False, 12)
625 hbox.pack_start(label, expand=True, fill=True)
626 hbox.pack_start(info, expand=False, fill=False)
627 sub_vbox.pack_start(hbox, expand=False, fill=False)
628
629 proxy_test_focus = []
630 self.direct_checkbox = gtk.RadioButton(None, "Direct network connection")
631 proxy_test_focus.append(self.direct_checkbox)
632 self.direct_checkbox.set_tooltip_text("Check this box to use a direct internet connection with no proxy")
633 self.direct_checkbox.set_active(not self.configuration.enable_proxy)
634 sub_vbox.pack_start(self.direct_checkbox, expand=False, fill=False)
635
636 self.proxy_checkbox = gtk.RadioButton(self.direct_checkbox, "Manual proxy configuration")
637 proxy_test_focus.append(self.proxy_checkbox)
638 self.proxy_checkbox.set_tooltip_text("Check this box to manually set up a specific proxy")
639 self.proxy_checkbox.set_active(self.configuration.enable_proxy)
640 sub_vbox.pack_start(self.proxy_checkbox, expand=False, fill=False)
641
642 self.same_checkbox = gtk.CheckButton("Use the HTTP proxy for all protocols")
643 proxy_test_focus.append(self.same_checkbox)
644 self.same_checkbox.set_tooltip_text("Check this box to use the HTTP proxy for all five proxies")
645 self.same_checkbox.set_active(self.configuration.same_proxy)
646 hbox = gtk.HBox(False, 12)
647 hbox.pack_start(self.same_checkbox, expand=False, fill=False, padding=24)
648 sub_vbox.pack_start(hbox, expand=False, fill=False)
649
650 self.proxy_table = gtk.Table(6, 5, False)
651 self.http_proxy, self.http_proxy_port, self.http_proxy_details = self.gen_proxy_entry_widget(
652 "http", self, True, 0)
653 proxy_test_focus +=[self.http_proxy, self.http_proxy_port]
654 self.http_proxy.connect("changed", self.http_proxy_changed)
655 self.http_proxy_port.connect("changed", self.http_proxy_changed)
656
657 self.https_proxy, self.https_proxy_port, self.https_proxy_details = self.gen_proxy_entry_widget(
658 "https", self, True, 1)
659 proxy_test_focus += [self.https_proxy, self.https_proxy_port]
660 self.same_proxy_addresses.append(self.https_proxy)
661 self.same_proxy_ports.append(self.https_proxy_port)
662
663 self.ftp_proxy, self.ftp_proxy_port, self.ftp_proxy_details = self.gen_proxy_entry_widget(
664 "ftp", self, True, 2)
665 proxy_test_focus += [self.ftp_proxy, self.ftp_proxy_port]
666 self.same_proxy_addresses.append(self.ftp_proxy)
667 self.same_proxy_ports.append(self.ftp_proxy_port)
668
669 self.socks_proxy, self.socks_proxy_port, self.socks_proxy_details = self.gen_proxy_entry_widget(
670 "socks", self, True, 3)
671 proxy_test_focus += [self.socks_proxy, self.socks_proxy_port]
672 self.same_proxy_addresses.append(self.socks_proxy)
673 self.same_proxy_ports.append(self.socks_proxy_port)
674
675 self.cvs_proxy, self.cvs_proxy_port, self.cvs_proxy_details = self.gen_proxy_entry_widget(
676 "cvs", self, True, 4)
677 proxy_test_focus += [self.cvs_proxy, self.cvs_proxy_port]
678 self.same_proxy_addresses.append(self.cvs_proxy)
679 self.same_proxy_ports.append(self.cvs_proxy_port)
680 self.all_proxy_ports = self.same_proxy_ports + [self.http_proxy_port]
681 self.all_proxy_addresses = self.same_proxy_addresses + [self.http_proxy]
682 sub_vbox.pack_start(self.proxy_table, expand=False, fill=False)
683 self.proxy_table.show_all()
684
685 # Create the graphical elements for the network test feature, but don't display them yet
686 self.test_network_button = HobAltButton("Test network configuration")
687 self.test_network_button.connect("clicked", self.test_network_button_cb)
688 self.test_proxy_progress = HobProgressBar()
689 self.dummy_progress = HobProgressBar()
690 self.retest_network_button = HobAltButton("Retest")
691 self.retest_network_button.connect("clicked", self.test_network_button_cb)
692 self.test_gui_elements = [self.test_network_button, self.test_proxy_progress, self.dummy_progress, self.retest_network_button]
693 # Initialize the network tester
694 self.test_proxy_state = self.TEST_NETWORK_NONE
695 self.set_test_proxy_state(self.TEST_NETWORK_INITIAL)
696 self.proxy_test_passed_id = self.handler.connect("network-passed", lambda h:self.test_proxy_ended(True))
697 self.proxy_test_failed_id = self.handler.connect("network-failed", lambda h:self.test_proxy_ended(False))
698 [w.connect("focus-in-event", self.test_proxy_focus_event) for w in proxy_test_focus]
699 [w.connect("focus-out-event", self.proxy_address_focus_out_event) for w in self.all_proxy_addresses]
700
701 self.direct_checkbox.connect("toggled", self.proxy_checkbox_toggled_cb)
702 self.proxy_checkbox.connect("toggled", self.proxy_checkbox_toggled_cb)
703 self.same_checkbox.connect("toggled", self.same_checkbox_toggled_cb)
704
705 self.refresh_proxy_components()
706 return advanced_vbox
707
708 def switch_to_page(self, page_id):
709 self.nb.set_current_page(page_id)
710
711 def details_cb(self, button, parent, protocol):
712 self.save_proxy_data()
713 dialog = ProxyDetailsDialog(title = protocol.upper() + " Proxy Details",
714 user = self.configuration.proxies[protocol][1],
715 passwd = self.configuration.proxies[protocol][2],
716 parent = parent,
717 flags = gtk.DIALOG_MODAL
718 | gtk.DIALOG_DESTROY_WITH_PARENT
719 | gtk.DIALOG_NO_SEPARATOR)
720 dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
721 response = dialog.run()
722 if response == gtk.RESPONSE_OK:
723 self.configuration.proxies[protocol][1] = dialog.user
724 self.configuration.proxies[protocol][2] = dialog.passwd
725 self.refresh_proxy_components()
726 dialog.destroy()
727
728 def rootfs_combo_changed_cb(self, rootfs_combo, all_package_format, check_hbox):
729 combo_item = self.rootfs_combo.get_active_text()
730 for child in check_hbox.get_children():
731 if isinstance(child, gtk.CheckButton):
732 check_hbox.remove(child)
733 for format in all_package_format:
734 if format != combo_item:
735 check_button = gtk.CheckButton(format)
736 check_hbox.pack_start(check_button, expand=False, fill=False)
737 check_hbox.show_all()
738
739 def gen_pkgfmt_widget(self, curr_package_format, all_package_format, tooltip_combo="", tooltip_extra=""):
740 pkgfmt_hbox = gtk.HBox(False, 24)
741
742 rootfs_vbox = gtk.VBox(False, 6)
743 pkgfmt_hbox.pack_start(rootfs_vbox, expand=False, fill=False)
744
745 label = self.gen_label_widget("Root file system package format")
746 rootfs_vbox.pack_start(label, expand=False, fill=False)
747
748 rootfs_format = ""
749 if curr_package_format:
750 rootfs_format = curr_package_format.split()[0]
751
752 rootfs_format_widget, rootfs_combo = self.gen_combo_widget(rootfs_format, all_package_format, tooltip_combo)
753 rootfs_vbox.pack_start(rootfs_format_widget, expand=False, fill=False)
754
755 extra_vbox = gtk.VBox(False, 6)
756 pkgfmt_hbox.pack_start(extra_vbox, expand=False, fill=False)
757
758 label = self.gen_label_widget("Additional package formats")
759 extra_vbox.pack_start(label, expand=False, fill=False)
760
761 check_hbox = gtk.HBox(False, 12)
762 extra_vbox.pack_start(check_hbox, expand=False, fill=False)
763 for format in all_package_format:
764 if format != rootfs_format:
765 check_button = gtk.CheckButton(format)
766 is_active = (format in curr_package_format.split())
767 check_button.set_active(is_active)
768 check_hbox.pack_start(check_button, expand=False, fill=False)
769
770 info = HobInfoButton(tooltip_extra, self)
771 check_hbox.pack_end(info, expand=False, fill=False)
772
773 rootfs_combo.connect("changed", self.rootfs_combo_changed_cb, all_package_format, check_hbox)
774
775 pkgfmt_hbox.show_all()
776
777 return pkgfmt_hbox, rootfs_combo, check_hbox
778
779 def editable_settings_cell_edited(self, cell, path_string, new_text, model):
780 it = model.get_iter_from_string(path_string)
781 column = cell.get_data("column")
782 model.set(it, column, new_text)
783
784 def editable_settings_add_item_clicked(self, button, model):
785 new_item = ["##KEY##", "##VALUE##"]
786
787 iter = model.append()
788 model.set (iter,
789 0, new_item[0],
790 1, new_item[1],
791 )
792
793 def editable_settings_remove_item_clicked(self, button, treeview):
794 selection = treeview.get_selection()
795 model, iter = selection.get_selected()
796
797 if iter:
798 path = model.get_path(iter)[0]
799 model.remove(iter)
800
801 def gen_editable_settings(self, setting, tooltip=""):
802 setting_hbox = gtk.HBox(False, 12)
803
804 vbox = gtk.VBox(False, 12)
805 setting_hbox.pack_start(vbox, expand=True, fill=True)
806
807 setting_store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
808 for key in setting.keys():
809 setting_store.set(setting_store.append(), 0, key, 1, setting[key])
810
811 setting_tree = gtk.TreeView(setting_store)
812 setting_tree.set_headers_visible(True)
813 setting_tree.set_size_request(300, 100)
814
815 col = gtk.TreeViewColumn('Key')
816 col.set_min_width(100)
817 col.set_max_width(150)
818 col.set_resizable(True)
819 col1 = gtk.TreeViewColumn('Value')
820 col1.set_min_width(100)
821 col1.set_max_width(150)
822 col1.set_resizable(True)
823 setting_tree.append_column(col)
824 setting_tree.append_column(col1)
825 cell = gtk.CellRendererText()
826 cell.set_property('width-chars', 10)
827 cell.set_property('editable', True)
828 cell.set_data("column", 0)
829 cell.connect("edited", self.editable_settings_cell_edited, setting_store)
830 cell1 = gtk.CellRendererText()
831 cell1.set_property('width-chars', 10)
832 cell1.set_property('editable', True)
833 cell1.set_data("column", 1)
834 cell1.connect("edited", self.editable_settings_cell_edited, setting_store)
835 col.pack_start(cell, True)
836 col1.pack_end(cell1, True)
837 col.set_attributes(cell, text=0)
838 col1.set_attributes(cell1, text=1)
839
840 scroll = gtk.ScrolledWindow()
841 scroll.set_shadow_type(gtk.SHADOW_IN)
842 scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
843 scroll.add(setting_tree)
844 vbox.pack_start(scroll, expand=True, fill=True)
845
846 # some buttons
847 hbox = gtk.HBox(True, 6)
848 vbox.pack_start(hbox, False, False)
849
850 button = gtk.Button(stock=gtk.STOCK_ADD)
851 button.connect("clicked", self.editable_settings_add_item_clicked, setting_store)
852 hbox.pack_start(button)
853
854 button = gtk.Button(stock=gtk.STOCK_REMOVE)
855 button.connect("clicked", self.editable_settings_remove_item_clicked, setting_tree)
856 hbox.pack_start(button)
857
858 info = HobInfoButton(tooltip, self)
859 setting_hbox.pack_start(info, expand=False, fill=False)
860
861 return setting_hbox, setting_store
862
863 def create_others_page(self):
864 advanced_vbox = gtk.VBox(False, 6)
865 advanced_vbox.set_border_width(6)
866
867 sub_vbox = gtk.VBox(False, 6)
868 advanced_vbox.pack_start(sub_vbox, expand=True, fill=True)
869 label = self.gen_label_widget("<span weight=\"bold\">Add your own variables:</span>")
870 tooltip = "These are key/value pairs for your extra settings. Click \'Add\' and then directly edit the key and the value"
871 setting_widget, self.setting_store = self.gen_editable_settings(self.configuration.extra_setting,"<b>Add your own variables</b>" + "*" + tooltip)
872 sub_vbox.pack_start(label, expand=False, fill=False)
873 sub_vbox.pack_start(setting_widget, expand=True, fill=True)
874
875 return advanced_vbox
876
877 def create_visual_elements(self):
878 self.nb = gtk.Notebook()
879 self.nb.set_show_tabs(True)
880 self.nb.append_page(self.create_build_environment_page(), gtk.Label("Build environment"))
881 self.nb.append_page(self.create_shared_state_page(), gtk.Label("Shared state"))
882 self.nb.append_page(self.create_network_page(), gtk.Label("Network"))
883 self.nb.append_page(self.create_others_page(), gtk.Label("Others"))
884 self.nb.set_current_page(0)
885 self.vbox.pack_start(self.nb, expand=True, fill=True)
886 self.vbox.pack_end(gtk.HSeparator(), expand=True, fill=True)
887
888 self.show_all()
889
890 def destroy(self):
891 self.handler.disconnect(self.proxy_test_passed_id)
892 self.handler.disconnect(self.proxy_test_failed_id)
893 super(SimpleSettingsDialog, self).destroy()
diff --git a/bitbake/lib/bb/ui/crumbs/hobcolor.py b/bitbake/lib/bb/ui/crumbs/hobcolor.py
new file mode 100644
index 0000000000..3316542a20
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hobcolor.py
@@ -0,0 +1,38 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2012 Intel Corporation
5#
6# Authored by Shane Wang <shane.wang@intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21class HobColors:
22 WHITE = "#ffffff"
23 PALE_GREEN = "#aaffaa"
24 ORANGE = "#eb8e68"
25 PALE_RED = "#ffaaaa"
26 GRAY = "#aaaaaa"
27 LIGHT_GRAY = "#dddddd"
28 SLIGHT_DARK = "#5f5f5f"
29 DARK = "#3c3b37"
30 BLACK = "#000000"
31 PALE_BLUE = "#53b8ff"
32 DEEP_RED = "#aa3e3e"
33 KHAKI = "#fff68f"
34
35 OK = WHITE
36 RUNNING = PALE_GREEN
37 WARNING = ORANGE
38 ERROR = PALE_RED
diff --git a/bitbake/lib/bb/ui/crumbs/hobeventhandler.py b/bitbake/lib/bb/ui/crumbs/hobeventhandler.py
new file mode 100644
index 0000000000..393c258e46
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hobeventhandler.py
@@ -0,0 +1,624 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22import gobject
23import logging
24import ast
25from bb.ui.crumbs.runningbuild import RunningBuild
26
27class HobHandler(gobject.GObject):
28
29 """
30 This object does BitBake event handling for the hob gui.
31 """
32 __gsignals__ = {
33 "package-formats-updated" : (gobject.SIGNAL_RUN_LAST,
34 gobject.TYPE_NONE,
35 (gobject.TYPE_PYOBJECT,)),
36 "config-updated" : (gobject.SIGNAL_RUN_LAST,
37 gobject.TYPE_NONE,
38 (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,)),
39 "command-succeeded" : (gobject.SIGNAL_RUN_LAST,
40 gobject.TYPE_NONE,
41 (gobject.TYPE_INT,)),
42 "command-failed" : (gobject.SIGNAL_RUN_LAST,
43 gobject.TYPE_NONE,
44 (gobject.TYPE_STRING,)),
45 "parsing-warning" : (gobject.SIGNAL_RUN_LAST,
46 gobject.TYPE_NONE,
47 (gobject.TYPE_STRING,)),
48 "sanity-failed" : (gobject.SIGNAL_RUN_LAST,
49 gobject.TYPE_NONE,
50 (gobject.TYPE_STRING, gobject.TYPE_INT)),
51 "generating-data" : (gobject.SIGNAL_RUN_LAST,
52 gobject.TYPE_NONE,
53 ()),
54 "data-generated" : (gobject.SIGNAL_RUN_LAST,
55 gobject.TYPE_NONE,
56 ()),
57 "parsing-started" : (gobject.SIGNAL_RUN_LAST,
58 gobject.TYPE_NONE,
59 (gobject.TYPE_PYOBJECT,)),
60 "parsing" : (gobject.SIGNAL_RUN_LAST,
61 gobject.TYPE_NONE,
62 (gobject.TYPE_PYOBJECT,)),
63 "parsing-completed" : (gobject.SIGNAL_RUN_LAST,
64 gobject.TYPE_NONE,
65 (gobject.TYPE_PYOBJECT,)),
66 "recipe-populated" : (gobject.SIGNAL_RUN_LAST,
67 gobject.TYPE_NONE,
68 ()),
69 "package-populated" : (gobject.SIGNAL_RUN_LAST,
70 gobject.TYPE_NONE,
71 ()),
72 "network-passed" : (gobject.SIGNAL_RUN_LAST,
73 gobject.TYPE_NONE,
74 ()),
75 "network-failed" : (gobject.SIGNAL_RUN_LAST,
76 gobject.TYPE_NONE,
77 ()),
78 }
79
80 (GENERATE_CONFIGURATION, GENERATE_RECIPES, GENERATE_PACKAGES, GENERATE_IMAGE, POPULATE_PACKAGEINFO, SANITY_CHECK, NETWORK_TEST) = range(7)
81 (SUB_PATH_LAYERS, SUB_FILES_DISTRO, SUB_FILES_MACH, SUB_FILES_SDKMACH, SUB_MATCH_CLASS, SUB_PARSE_CONFIG, SUB_SANITY_CHECK,
82 SUB_GNERATE_TGTS, SUB_GENERATE_PKGINFO, SUB_BUILD_RECIPES, SUB_BUILD_IMAGE, SUB_NETWORK_TEST) = range(12)
83
84 def __init__(self, server, recipe_model, package_model):
85 super(HobHandler, self).__init__()
86
87 self.build = RunningBuild(sequential=True)
88
89 self.recipe_model = recipe_model
90 self.package_model = package_model
91
92 self.commands_async = []
93 self.generating = False
94 self.current_phase = None
95 self.building = False
96 self.recipe_queue = []
97 self.package_queue = []
98
99 self.server = server
100 self.error_msg = ""
101 self.initcmd = None
102 self.parsing = False
103
104 def set_busy(self):
105 if not self.generating:
106 self.emit("generating-data")
107 self.generating = True
108
109 def clear_busy(self):
110 if self.generating:
111 self.emit("data-generated")
112 self.generating = False
113
114 def runCommand(self, commandline):
115 try:
116 result, error = self.server.runCommand(commandline)
117 if error:
118 raise Exception("Error running command '%s': %s" % (commandline, error))
119 return result
120 except Exception as e:
121 self.commands_async = []
122 self.clear_busy()
123 self.emit("command-failed", "Hob Exception - %s" % (str(e)))
124 return None
125
126 def run_next_command(self, initcmd=None):
127 if initcmd != None:
128 self.initcmd = initcmd
129
130 if self.commands_async:
131 self.set_busy()
132 next_command = self.commands_async.pop(0)
133 else:
134 self.clear_busy()
135 if self.initcmd != None:
136 self.emit("command-succeeded", self.initcmd)
137 return
138
139 if next_command == self.SUB_PATH_LAYERS:
140 self.runCommand(["findConfigFilePath", "bblayers.conf"])
141 elif next_command == self.SUB_FILES_DISTRO:
142 self.runCommand(["findConfigFiles", "DISTRO"])
143 elif next_command == self.SUB_FILES_MACH:
144 self.runCommand(["findConfigFiles", "MACHINE"])
145 elif next_command == self.SUB_FILES_SDKMACH:
146 self.runCommand(["findConfigFiles", "MACHINE-SDK"])
147 elif next_command == self.SUB_MATCH_CLASS:
148 self.runCommand(["findFilesMatchingInDir", "rootfs_", "classes"])
149 elif next_command == self.SUB_PARSE_CONFIG:
150 self.runCommand(["enableDataTracking"])
151 self.runCommand(["parseConfigurationFiles", "conf/.hob.conf", ""])
152 self.runCommand(["disableDataTracking"])
153 elif next_command == self.SUB_GNERATE_TGTS:
154 self.runCommand(["generateTargetsTree", "classes/image.bbclass", []])
155 elif next_command == self.SUB_GENERATE_PKGINFO:
156 self.runCommand(["triggerEvent", "bb.event.RequestPackageInfo()"])
157 elif next_command == self.SUB_SANITY_CHECK:
158 self.runCommand(["triggerEvent", "bb.event.SanityCheck()"])
159 elif next_command == self.SUB_NETWORK_TEST:
160 self.runCommand(["triggerEvent", "bb.event.NetworkTest()"])
161 elif next_command == self.SUB_BUILD_RECIPES:
162 self.clear_busy()
163 self.building = True
164 self.runCommand(["buildTargets", self.recipe_queue, self.default_task])
165 self.recipe_queue = []
166 elif next_command == self.SUB_BUILD_IMAGE:
167 self.clear_busy()
168 self.building = True
169 targets = [self.image]
170 if self.toolchain_packages:
171 self.set_var_in_file("TOOLCHAIN_TARGET_TASK", " ".join(self.toolchain_packages), "local.conf")
172 targets.append(self.toolchain)
173 if targets[0] == "hob-image":
174 self.set_var_in_file("LINGUAS_INSTALL", "", "local.conf")
175 hobImage = self.runCommand(["matchFile", "hob-image.bb"])
176 if self.base_image != "Start with an empty image recipe":
177 baseImage = self.runCommand(["matchFile", self.base_image + ".bb"])
178 version = self.runCommand(["generateNewImage", hobImage, baseImage, self.package_queue, True, ""])
179 targets[0] += version
180 self.recipe_model.set_custom_image_version(version)
181
182 self.runCommand(["buildTargets", targets, self.default_task])
183
184 def display_error(self):
185 self.clear_busy()
186 self.emit("command-failed", self.error_msg)
187 self.error_msg = ""
188 if self.building:
189 self.building = False
190
191 def handle_event(self, event):
192 if not event:
193 return
194 if self.building:
195 self.current_phase = "building"
196 self.build.handle_event(event)
197
198 if isinstance(event, bb.event.PackageInfo):
199 self.package_model.populate(event._pkginfolist)
200 self.emit("package-populated")
201 self.run_next_command()
202
203 elif isinstance(event, bb.event.SanityCheckPassed):
204 reparse = self.runCommand(["getVariable", "BB_INVALIDCONF"]) or None
205 if reparse is True:
206 self.set_var_in_file("BB_INVALIDCONF", False, "local.conf")
207 self.runCommand(["parseConfigurationFiles", "conf/.hob.conf", ""])
208 self.run_next_command()
209
210 elif isinstance(event, bb.event.SanityCheckFailed):
211 self.emit("sanity-failed", event._msg, event._network_error)
212
213 elif isinstance(event, logging.LogRecord):
214 if not self.building:
215 if event.levelno >= logging.ERROR:
216 formatter = bb.msg.BBLogFormatter()
217 msg = formatter.format(event)
218 self.error_msg += msg + '\n'
219 elif event.levelno >= logging.WARNING and self.parsing == True:
220 formatter = bb.msg.BBLogFormatter()
221 msg = formatter.format(event)
222 warn_msg = msg + '\n'
223 self.emit("parsing-warning", warn_msg)
224
225 elif isinstance(event, bb.event.TargetsTreeGenerated):
226 self.current_phase = "data generation"
227 if event._model:
228 self.recipe_model.populate(event._model)
229 self.emit("recipe-populated")
230 elif isinstance(event, bb.event.ConfigFilesFound):
231 self.current_phase = "configuration lookup"
232 var = event._variable
233 values = event._values
234 values.sort()
235 self.emit("config-updated", var, values)
236 elif isinstance(event, bb.event.ConfigFilePathFound):
237 self.current_phase = "configuration lookup"
238 elif isinstance(event, bb.event.FilesMatchingFound):
239 self.current_phase = "configuration lookup"
240 # FIXME: hard coding, should at least be a variable shared between
241 # here and the caller
242 if event._pattern == "rootfs_":
243 formats = []
244 for match in event._matches:
245 classname, sep, cls = match.rpartition(".")
246 fs, sep, format = classname.rpartition("_")
247 formats.append(format)
248 formats.sort()
249 self.emit("package-formats-updated", formats)
250 elif isinstance(event, bb.command.CommandCompleted):
251 self.current_phase = None
252 self.run_next_command()
253 elif isinstance(event, bb.command.CommandFailed):
254 self.commands_async = []
255 self.display_error()
256 elif isinstance(event, (bb.event.ParseStarted,
257 bb.event.CacheLoadStarted,
258 bb.event.TreeDataPreparationStarted,
259 )):
260 message = {}
261 message["eventname"] = bb.event.getName(event)
262 message["current"] = 0
263 message["total"] = None
264 message["title"] = "Parsing recipes"
265 self.emit("parsing-started", message)
266 if isinstance(event, bb.event.ParseStarted):
267 self.parsing = True
268 elif isinstance(event, (bb.event.ParseProgress,
269 bb.event.CacheLoadProgress,
270 bb.event.TreeDataPreparationProgress)):
271 message = {}
272 message["eventname"] = bb.event.getName(event)
273 message["current"] = event.current
274 message["total"] = event.total
275 message["title"] = "Parsing recipes"
276 self.emit("parsing", message)
277 elif isinstance(event, (bb.event.ParseCompleted,
278 bb.event.CacheLoadCompleted,
279 bb.event.TreeDataPreparationCompleted)):
280 message = {}
281 message["eventname"] = bb.event.getName(event)
282 message["current"] = event.total
283 message["total"] = event.total
284 message["title"] = "Parsing recipes"
285 self.emit("parsing-completed", message)
286 if isinstance(event, bb.event.ParseCompleted):
287 self.parsing = False
288 elif isinstance(event, bb.event.NetworkTestFailed):
289 self.emit("network-failed")
290 self.run_next_command()
291 elif isinstance(event, bb.event.NetworkTestPassed):
292 self.emit("network-passed")
293 self.run_next_command()
294
295 if self.error_msg and not self.commands_async:
296 self.display_error()
297
298 return
299
300 def init_cooker(self):
301 self.runCommand(["initCooker"])
302 self.runCommand(["createConfigFile", ".hob.conf"])
303
304 def reset_cooker(self):
305 self.runCommand(["enableDataTracking"])
306 self.runCommand(["resetCooker"])
307 self.runCommand(["disableDataTracking"])
308
309 def set_extra_inherit(self, bbclass):
310 inherits = self.runCommand(["getVariable", "INHERIT"]) or ""
311 inherits = inherits + " " + bbclass
312 self.set_var_in_file("INHERIT", inherits, ".hob.conf")
313
314 def set_bblayers(self, bblayers):
315 self.set_var_in_file("BBLAYERS", " ".join(bblayers), "bblayers.conf")
316
317 def set_machine(self, machine):
318 if machine:
319 self.early_assign_var_in_file("MACHINE", machine, "local.conf")
320
321 def set_sdk_machine(self, sdk_machine):
322 self.set_var_in_file("SDKMACHINE", sdk_machine, "local.conf")
323
324 def set_image_fstypes(self, image_fstypes):
325 self.set_var_in_file("IMAGE_FSTYPES", image_fstypes, "local.conf")
326
327 def set_distro(self, distro):
328 self.set_var_in_file("DISTRO", distro, "local.conf")
329
330 def set_package_format(self, format):
331 package_classes = ""
332 for pkgfmt in format.split():
333 package_classes += ("package_%s" % pkgfmt + " ")
334 self.set_var_in_file("PACKAGE_CLASSES", package_classes, "local.conf")
335
336 def set_bbthreads(self, threads):
337 self.set_var_in_file("BB_NUMBER_THREADS", threads, "local.conf")
338
339 def set_pmake(self, threads):
340 pmake = "-j %s" % threads
341 self.set_var_in_file("PARALLEL_MAKE", pmake, "local.conf")
342
343 def set_dl_dir(self, directory):
344 self.set_var_in_file("DL_DIR", directory, "local.conf")
345
346 def set_sstate_dir(self, directory):
347 self.set_var_in_file("SSTATE_DIR", directory, "local.conf")
348
349 def set_sstate_mirrors(self, url):
350 self.set_var_in_file("SSTATE_MIRRORS", url, "local.conf")
351
352 def set_extra_size(self, image_extra_size):
353 self.set_var_in_file("IMAGE_ROOTFS_EXTRA_SPACE", str(image_extra_size), "local.conf")
354
355 def set_rootfs_size(self, image_rootfs_size):
356 self.set_var_in_file("IMAGE_ROOTFS_SIZE", str(image_rootfs_size), "local.conf")
357
358 def set_incompatible_license(self, incompat_license):
359 self.set_var_in_file("INCOMPATIBLE_LICENSE", incompat_license, "local.conf")
360
361 def set_extra_setting(self, extra_setting):
362 self.set_var_in_file("EXTRA_SETTING", extra_setting, "local.conf")
363
364 def set_extra_config(self, extra_setting):
365 old_extra_setting = ast.literal_eval(self.runCommand(["getVariable", "EXTRA_SETTING"]) or "{}")
366 if extra_setting:
367 self.set_var_in_file("EXTRA_SETTING", extra_setting, "local.conf")
368 else:
369 self.remove_var_from_file("EXTRA_SETTING")
370
371 #remove not needed settings from conf
372 for key in old_extra_setting:
373 if key not in extra_setting:
374 self.remove_var_from_file(key)
375 for key in extra_setting.keys():
376 value = extra_setting[key]
377 self.set_var_in_file(key, value, "local.conf")
378
379 def set_http_proxy(self, http_proxy):
380 self.set_var_in_file("http_proxy", http_proxy, "local.conf")
381
382 def set_https_proxy(self, https_proxy):
383 self.set_var_in_file("https_proxy", https_proxy, "local.conf")
384
385 def set_ftp_proxy(self, ftp_proxy):
386 self.set_var_in_file("ftp_proxy", ftp_proxy, "local.conf")
387
388 def set_socks_proxy(self, socks_proxy):
389 self.set_var_in_file("all_proxy", socks_proxy, "local.conf")
390
391 def set_cvs_proxy(self, host, port):
392 self.set_var_in_file("CVS_PROXY_HOST", host, "local.conf")
393 self.set_var_in_file("CVS_PROXY_PORT", port, "local.conf")
394
395 def request_package_info(self):
396 self.commands_async.append(self.SUB_GENERATE_PKGINFO)
397 self.run_next_command(self.POPULATE_PACKAGEINFO)
398
399 def trigger_sanity_check(self):
400 self.commands_async.append(self.SUB_SANITY_CHECK)
401 self.run_next_command(self.SANITY_CHECK)
402
403 def trigger_network_test(self):
404 self.commands_async.append(self.SUB_NETWORK_TEST)
405 self.run_next_command(self.NETWORK_TEST)
406
407 def generate_configuration(self):
408 self.commands_async.append(self.SUB_PARSE_CONFIG)
409 self.commands_async.append(self.SUB_PATH_LAYERS)
410 self.commands_async.append(self.SUB_FILES_DISTRO)
411 self.commands_async.append(self.SUB_FILES_MACH)
412 self.commands_async.append(self.SUB_FILES_SDKMACH)
413 self.commands_async.append(self.SUB_MATCH_CLASS)
414 self.run_next_command(self.GENERATE_CONFIGURATION)
415
416 def generate_recipes(self):
417 self.commands_async.append(self.SUB_PARSE_CONFIG)
418 self.commands_async.append(self.SUB_GNERATE_TGTS)
419 self.run_next_command(self.GENERATE_RECIPES)
420
421 def generate_packages(self, tgts, default_task="build"):
422 targets = []
423 targets.extend(tgts)
424 self.recipe_queue = targets
425 self.default_task = default_task
426 self.commands_async.append(self.SUB_PARSE_CONFIG)
427 self.commands_async.append(self.SUB_BUILD_RECIPES)
428 self.run_next_command(self.GENERATE_PACKAGES)
429
430 def generate_image(self, image, base_image, toolchain, image_packages=[], toolchain_packages=[], default_task="build"):
431 self.image = image
432 self.base_image = base_image
433 self.toolchain = toolchain
434 self.package_queue = image_packages
435 self.toolchain_packages = toolchain_packages
436 self.default_task = default_task
437 self.commands_async.append(self.SUB_PARSE_CONFIG)
438 self.commands_async.append(self.SUB_BUILD_IMAGE)
439 self.run_next_command(self.GENERATE_IMAGE)
440
441 def generate_new_image(self, image, base_image, package_queue, description):
442 base_image = self.runCommand(["matchFile", self.base_image + ".bb"])
443 self.runCommand(["generateNewImage", image, base_image, package_queue, False, description])
444
445 def ensure_dir(self, directory):
446 self.runCommand(["ensureDir", directory])
447
448 def build_succeeded_async(self):
449 self.building = False
450
451 def build_failed_async(self):
452 self.initcmd = None
453 self.commands_async = []
454 self.building = False
455
456 def cancel_parse(self):
457 self.runCommand(["stateForceShutdown"])
458
459 def cancel_build(self, force=False):
460 if force:
461 # Force the cooker to stop as quickly as possible
462 self.runCommand(["stateForceShutdown"])
463 else:
464 # Wait for tasks to complete before shutting down, this helps
465 # leave the workdir in a usable state
466 self.runCommand(["stateShutdown"])
467
468 def reset_build(self):
469 self.build.reset()
470
471 def get_logfile(self):
472 return self.server.runCommand(["getVariable", "BB_CONSOLELOG"])[0]
473
474 def get_topdir(self):
475 return self.runCommand(["getVariable", "TOPDIR"]) or ""
476
477 def _remove_redundant(self, string):
478 ret = []
479 for i in string.split():
480 if i not in ret:
481 ret.append(i)
482 return " ".join(ret)
483
484 def set_var_in_file(self, var, val, default_file=None):
485 self.runCommand(["enableDataTracking"])
486 self.server.runCommand(["setVarFile", var, val, default_file, "set"])
487 self.runCommand(["disableDataTracking"])
488
489 def early_assign_var_in_file(self, var, val, default_file=None):
490 self.runCommand(["enableDataTracking"])
491 self.server.runCommand(["setVarFile", var, val, default_file, "earlyAssign"])
492 self.runCommand(["disableDataTracking"])
493
494 def remove_var_from_file(self, var):
495 self.server.runCommand(["removeVarFile", var])
496
497 def append_var_in_file(self, var, val, default_file=None):
498 self.server.runCommand(["setVarFile", var, val, default_file, "append"])
499
500 def append_to_bbfiles(self, val):
501 bbfiles = self.runCommand(["getVariable", "BBFILES", "False"]) or ""
502 bbfiles = bbfiles.split()
503 if val not in bbfiles:
504 self.append_var_in_file("BBFILES", val, "local.conf")
505
506 def get_parameters(self):
507 # retrieve the parameters from bitbake
508 params = {}
509 params["core_base"] = self.runCommand(["getVariable", "COREBASE"]) or ""
510 hob_layer = params["core_base"] + "/meta-hob"
511 params["layer"] = self.runCommand(["getVariable", "BBLAYERS"]) or ""
512 params["layers_non_removable"] = self.runCommand(["getVariable", "BBLAYERS_NON_REMOVABLE"]) or ""
513 if hob_layer not in params["layer"].split():
514 params["layer"] += (" " + hob_layer)
515 if hob_layer not in params["layers_non_removable"].split():
516 params["layers_non_removable"] += (" " + hob_layer)
517 params["dldir"] = self.runCommand(["getVariable", "DL_DIR"]) or ""
518 params["machine"] = self.runCommand(["getVariable", "MACHINE"]) or ""
519 params["distro"] = self.runCommand(["getVariable", "DISTRO"]) or "defaultsetup"
520 params["pclass"] = self.runCommand(["getVariable", "PACKAGE_CLASSES"]) or ""
521 params["sstatedir"] = self.runCommand(["getVariable", "SSTATE_DIR"]) or ""
522 params["sstatemirror"] = self.runCommand(["getVariable", "SSTATE_MIRRORS"]) or ""
523
524 num_threads = self.runCommand(["getCpuCount"])
525 if not num_threads:
526 num_threads = 1
527 max_threads = 65536
528 else:
529 try:
530 num_threads = int(num_threads)
531 max_threads = 16 * num_threads
532 except:
533 num_threads = 1
534 max_threads = 65536
535 params["max_threads"] = max_threads
536
537 bbthread = self.runCommand(["getVariable", "BB_NUMBER_THREADS"])
538 if not bbthread:
539 bbthread = num_threads
540 else:
541 try:
542 bbthread = int(bbthread)
543 except:
544 bbthread = num_threads
545 params["bbthread"] = bbthread
546
547 pmake = self.runCommand(["getVariable", "PARALLEL_MAKE"])
548 if not pmake:
549 pmake = num_threads
550 elif isinstance(pmake, int):
551 pass
552 else:
553 try:
554 pmake = int(pmake.lstrip("-j "))
555 except:
556 pmake = num_threads
557 params["pmake"] = "-j %s" % pmake
558
559 params["image_addr"] = self.runCommand(["getVariable", "DEPLOY_DIR_IMAGE"]) or ""
560
561 image_extra_size = self.runCommand(["getVariable", "IMAGE_ROOTFS_EXTRA_SPACE"])
562 if not image_extra_size:
563 image_extra_size = 0
564 else:
565 try:
566 image_extra_size = int(image_extra_size)
567 except:
568 image_extra_size = 0
569 params["image_extra_size"] = image_extra_size
570
571 image_rootfs_size = self.runCommand(["getVariable", "IMAGE_ROOTFS_SIZE"])
572 if not image_rootfs_size:
573 image_rootfs_size = 0
574 else:
575 try:
576 image_rootfs_size = int(image_rootfs_size)
577 except:
578 image_rootfs_size = 0
579 params["image_rootfs_size"] = image_rootfs_size
580
581 image_overhead_factor = self.runCommand(["getVariable", "IMAGE_OVERHEAD_FACTOR"])
582 if not image_overhead_factor:
583 image_overhead_factor = 1
584 else:
585 try:
586 image_overhead_factor = float(image_overhead_factor)
587 except:
588 image_overhead_factor = 1
589 params['image_overhead_factor'] = image_overhead_factor
590
591 params["incompat_license"] = self._remove_redundant(self.runCommand(["getVariable", "INCOMPATIBLE_LICENSE"]) or "")
592 params["sdk_machine"] = self.runCommand(["getVariable", "SDKMACHINE"]) or self.runCommand(["getVariable", "SDK_ARCH"]) or ""
593
594 params["image_fstypes"] = self._remove_redundant(self.runCommand(["getVariable", "IMAGE_FSTYPES"]) or "")
595
596 params["image_types"] = self._remove_redundant(self.runCommand(["getVariable", "IMAGE_TYPES"]) or "")
597
598 params["conf_version"] = self.runCommand(["getVariable", "CONF_VERSION"]) or ""
599 params["lconf_version"] = self.runCommand(["getVariable", "LCONF_VERSION"]) or ""
600
601 params["runnable_image_types"] = self._remove_redundant(self.runCommand(["getVariable", "RUNNABLE_IMAGE_TYPES"]) or "")
602 params["runnable_machine_patterns"] = self._remove_redundant(self.runCommand(["getVariable", "RUNNABLE_MACHINE_PATTERNS"]) or "")
603 params["deployable_image_types"] = self._remove_redundant(self.runCommand(["getVariable", "DEPLOYABLE_IMAGE_TYPES"]) or "")
604 params["kernel_image_type"] = self.runCommand(["getVariable", "KERNEL_IMAGETYPE"]) or ""
605 params["tmpdir"] = self.runCommand(["getVariable", "TMPDIR"]) or ""
606 params["distro_version"] = self.runCommand(["getVariable", "DISTRO_VERSION"]) or ""
607 params["target_os"] = self.runCommand(["getVariable", "TARGET_OS"]) or ""
608 params["target_arch"] = self.runCommand(["getVariable", "TARGET_ARCH"]) or ""
609 params["tune_pkgarch"] = self.runCommand(["getVariable", "TUNE_PKGARCH"]) or ""
610 params["bb_version"] = self.runCommand(["getVariable", "BB_MIN_VERSION"]) or ""
611
612 params["default_task"] = self.runCommand(["getVariable", "BB_DEFAULT_TASK"]) or "build"
613
614 params["socks_proxy"] = self.runCommand(["getVariable", "all_proxy"]) or ""
615 params["http_proxy"] = self.runCommand(["getVariable", "http_proxy"]) or ""
616 params["ftp_proxy"] = self.runCommand(["getVariable", "ftp_proxy"]) or ""
617 params["https_proxy"] = self.runCommand(["getVariable", "https_proxy"]) or ""
618
619 params["cvs_proxy_host"] = self.runCommand(["getVariable", "CVS_PROXY_HOST"]) or ""
620 params["cvs_proxy_port"] = self.runCommand(["getVariable", "CVS_PROXY_PORT"]) or ""
621
622 params["image_white_pattern"] = self.runCommand(["getVariable", "BBUI_IMAGE_WHITE_PATTERN"]) or ""
623 params["image_black_pattern"] = self.runCommand(["getVariable", "BBUI_IMAGE_BLACK_PATTERN"]) or ""
624 return params
diff --git a/bitbake/lib/bb/ui/crumbs/hoblistmodel.py b/bitbake/lib/bb/ui/crumbs/hoblistmodel.py
new file mode 100644
index 0000000000..b4d2a621b7
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hoblistmodel.py
@@ -0,0 +1,900 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2011 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import gobject
25from bb.ui.crumbs.hobpages import HobPage
26
27#
28# PackageListModel
29#
30class PackageListModel(gtk.ListStore):
31 """
32 This class defines an gtk.ListStore subclass which will convert the output
33 of the bb.event.TargetsTreeGenerated event into a gtk.ListStore whilst also
34 providing convenience functions to access gtk.TreeModel subclasses which
35 provide filtered views of the data.
36 """
37
38 (COL_NAME, COL_VER, COL_REV, COL_RNM, COL_SEC, COL_SUM, COL_RDEP, COL_RPROV, COL_SIZE, COL_RCP, COL_BINB, COL_INC, COL_FADE_INC, COL_FONT, COL_FLIST) = range(15)
39
40 __gsignals__ = {
41 "package-selection-changed" : (gobject.SIGNAL_RUN_LAST,
42 gobject.TYPE_NONE,
43 ()),
44 }
45
46 __toolchain_required_packages__ = ["packagegroup-core-standalone-sdk-target", "packagegroup-core-standalone-sdk-target-dbg"]
47
48 def __init__(self):
49 self.rprov_pkg = {}
50 gtk.ListStore.__init__ (self,
51 gobject.TYPE_STRING,
52 gobject.TYPE_STRING,
53 gobject.TYPE_STRING,
54 gobject.TYPE_STRING,
55 gobject.TYPE_STRING,
56 gobject.TYPE_STRING,
57 gobject.TYPE_STRING,
58 gobject.TYPE_STRING,
59 gobject.TYPE_STRING,
60 gobject.TYPE_STRING,
61 gobject.TYPE_STRING,
62 gobject.TYPE_BOOLEAN,
63 gobject.TYPE_BOOLEAN,
64 gobject.TYPE_STRING,
65 gobject.TYPE_STRING)
66 self.sort_column_id, self.sort_order = PackageListModel.COL_NAME, gtk.SORT_ASCENDING
67
68 """
69 Find the model path for the item_name
70 Returns the path in the model or None
71 """
72 def find_path_for_item(self, item_name):
73 pkg = item_name
74 if item_name not in self.pn_path.keys():
75 if item_name not in self.rprov_pkg.keys():
76 return None
77 pkg = self.rprov_pkg[item_name]
78 if pkg not in self.pn_path.keys():
79 return None
80
81 return self.pn_path[pkg]
82
83 def find_item_for_path(self, item_path):
84 return self[item_path][self.COL_NAME]
85
86 """
87 Helper function to determine whether an item is an item specified by filter
88 """
89 def tree_model_filter(self, model, it, filter):
90 name = model.get_value(it, self.COL_NAME)
91
92 for key in filter.keys():
93 if key == self.COL_NAME:
94 if filter[key] != 'Search packages by name':
95 if name and filter[key] not in name:
96 return False
97 else:
98 if model.get_value(it, key) not in filter[key]:
99 return False
100 self.filtered_nb += 1
101 return True
102
103 """
104 Create, if required, and return a filtered gtk.TreeModelSort
105 containing only the items specified by filter
106 """
107 def tree_model(self, filter, excluded_items_ahead=False, included_items_ahead=False, search_data=None, initial=False):
108 model = self.filter_new()
109 self.filtered_nb = 0
110 model.set_visible_func(self.tree_model_filter, filter)
111
112 sort = gtk.TreeModelSort(model)
113 sort.connect ('sort-column-changed', self.sort_column_changed_cb)
114 if initial:
115 sort.set_sort_column_id(PackageListModel.COL_NAME, gtk.SORT_ASCENDING)
116 sort.set_default_sort_func(None)
117 elif excluded_items_ahead:
118 sort.set_default_sort_func(self.exclude_item_sort_func, search_data)
119 elif included_items_ahead:
120 sort.set_default_sort_func(self.include_item_sort_func, search_data)
121 else:
122 if search_data and search_data!='Search recipes by name' and search_data!='Search package groups by name':
123 sort.set_default_sort_func(self.sort_func, search_data)
124 else:
125 sort.set_sort_column_id(self.sort_column_id, self.sort_order)
126 sort.set_default_sort_func(None)
127
128 sort.set_sort_func(PackageListModel.COL_INC, self.sort_column, PackageListModel.COL_INC)
129 sort.set_sort_func(PackageListModel.COL_SIZE, self.sort_column, PackageListModel.COL_SIZE)
130 sort.set_sort_func(PackageListModel.COL_BINB, self.sort_binb_column)
131 sort.set_sort_func(PackageListModel.COL_RCP, self.sort_column, PackageListModel.COL_RCP)
132 return sort
133
134 def sort_column_changed_cb (self, data):
135 self.sort_column_id, self.sort_order = data.get_sort_column_id ()
136
137 def sort_column(self, model, row1, row2, col):
138 value1 = model.get_value(row1, col)
139 value2 = model.get_value(row2, col)
140 if col==PackageListModel.COL_SIZE:
141 value1 = HobPage._string_to_size(value1)
142 value2 = HobPage._string_to_size(value2)
143
144 cmp_res = cmp(value1, value2)
145 if cmp_res!=0:
146 if col==PackageListModel.COL_INC:
147 return -cmp_res
148 else:
149 return cmp_res
150 else:
151 name1 = model.get_value(row1, PackageListModel.COL_NAME)
152 name2 = model.get_value(row2, PackageListModel.COL_NAME)
153 return cmp(name1,name2)
154
155 def sort_binb_column(self, model, row1, row2):
156 value1 = model.get_value(row1, PackageListModel.COL_BINB)
157 value2 = model.get_value(row2, PackageListModel.COL_BINB)
158 value1_list = value1.split(', ')
159 value2_list = value2.split(', ')
160
161 value1 = value1_list[0]
162 value2 = value2_list[0]
163
164 cmp_res = cmp(value1, value2)
165 if cmp_res==0:
166 cmp_size = cmp(len(value1_list), len(value2_list))
167 if cmp_size==0:
168 name1 = model.get_value(row1, PackageListModel.COL_NAME)
169 name2 = model.get_value(row2, PackageListModel.COL_NAME)
170 return cmp(name1,name2)
171 else:
172 return cmp_size
173 else:
174 return cmp_res
175
176 def exclude_item_sort_func(self, model, iter1, iter2, user_data=None):
177 if user_data:
178 val1 = model.get_value(iter1, PackageListModel.COL_NAME)
179 val2 = model.get_value(iter2, PackageListModel.COL_NAME)
180 return self.cmp_vals(val1, val2, user_data)
181 else:
182 val1 = model.get_value(iter1, PackageListModel.COL_FADE_INC)
183 val2 = model.get_value(iter2, PackageListModel.COL_INC)
184 return ((val1 == True) and (val2 == False))
185
186 def include_item_sort_func(self, model, iter1, iter2, user_data=None):
187 if user_data:
188 val1 = model.get_value(iter1, PackageListModel.COL_NAME)
189 val2 = model.get_value(iter2, PackageListModel.COL_NAME)
190 return self.cmp_vals(val1, val2, user_data)
191 else:
192 val1 = model.get_value(iter1, PackageListModel.COL_INC)
193 val2 = model.get_value(iter2, PackageListModel.COL_INC)
194 return ((val1 == False) and (val2 == True))
195
196 def sort_func(self, model, iter1, iter2, user_data):
197 val1 = model.get_value(iter1, PackageListModel.COL_NAME)
198 val2 = model.get_value(iter2, PackageListModel.COL_NAME)
199 return self.cmp_vals(val1, val2, user_data)
200
201 def cmp_vals(self, val1, val2, user_data):
202 if val1.startswith(user_data) and not val2.startswith(user_data):
203 return -1
204 elif not val1.startswith(user_data) and val2.startswith(user_data):
205 return 1
206 else:
207 return cmp(val1, val2)
208
209 def convert_vpath_to_path(self, view_model, view_path):
210 # view_model is the model sorted
211 # get the path of the model filtered
212 filtered_model_path = view_model.convert_path_to_child_path(view_path)
213 # get the model filtered
214 filtered_model = view_model.get_model()
215 # get the path of the original model
216 path = filtered_model.convert_path_to_child_path(filtered_model_path)
217 return path
218
219 def convert_path_to_vpath(self, view_model, path):
220 it = view_model.get_iter_first()
221 while it:
222 name = self.find_item_for_path(path)
223 view_name = view_model.get_value(it, PackageListModel.COL_NAME)
224 if view_name == name:
225 view_path = view_model.get_path(it)
226 return view_path
227 it = view_model.iter_next(it)
228 return None
229
230 """
231 The populate() function takes as input the data from a
232 bb.event.PackageInfo event and populates the package list.
233 """
234 def populate(self, pkginfolist):
235 # First clear the model, in case repopulating
236 self.clear()
237
238 def getpkgvalue(pkgdict, key, pkgname, defaultval = None):
239 value = pkgdict.get('%s_%s' % (key, pkgname), None)
240 if not value:
241 value = pkgdict.get(key, defaultval)
242 return value
243
244 for pkginfo in pkginfolist:
245 pn = pkginfo['PN']
246 pv = pkginfo['PV']
247 pr = pkginfo['PR']
248 pkg = pkginfo['PKG']
249 pkgv = getpkgvalue(pkginfo, 'PKGV', pkg)
250 pkgr = getpkgvalue(pkginfo, 'PKGR', pkg)
251 # PKGSIZE is artificial, will always be overridden with the package name if present
252 pkgsize = pkginfo.get('PKGSIZE_%s' % pkg, "0")
253 # PKG_%s is the renamed version
254 pkg_rename = pkginfo.get('PKG_%s' % pkg, "")
255 # The rest may be overridden or not
256 section = getpkgvalue(pkginfo, 'SECTION', pkg, "")
257 summary = getpkgvalue(pkginfo, 'SUMMARY', pkg, "")
258 rdep = getpkgvalue(pkginfo, 'RDEPENDS', pkg, "")
259 rrec = getpkgvalue(pkginfo, 'RRECOMMENDS', pkg, "")
260 rprov = getpkgvalue(pkginfo, 'RPROVIDES', pkg, "")
261 files_list = getpkgvalue(pkginfo, 'FILES_INFO', pkg, "")
262 for i in rprov.split():
263 self.rprov_pkg[i] = pkg
264
265 recipe = pn + '-' + pv + '-' + pr
266
267 allow_empty = getpkgvalue(pkginfo, 'ALLOW_EMPTY', pkg, "")
268
269 if pkgsize == "0" and not allow_empty:
270 continue
271
272 # pkgsize is in KB
273 size = HobPage._size_to_string(HobPage._string_to_size(pkgsize + ' KB'))
274 self.set(self.append(), self.COL_NAME, pkg, self.COL_VER, pkgv,
275 self.COL_REV, pkgr, self.COL_RNM, pkg_rename,
276 self.COL_SEC, section, self.COL_SUM, summary,
277 self.COL_RDEP, rdep + ' ' + rrec,
278 self.COL_RPROV, rprov, self.COL_SIZE, size,
279 self.COL_RCP, recipe, self.COL_BINB, "",
280 self.COL_INC, False, self.COL_FONT, '10', self.COL_FLIST, files_list)
281
282 self.pn_path = {}
283 it = self.get_iter_first()
284 while it:
285 pn = self.get_value(it, self.COL_NAME)
286 path = self.get_path(it)
287 self.pn_path[pn] = path
288 it = self.iter_next(it)
289
290 """
291 Update the model, send out the notification.
292 """
293 def selection_change_notification(self):
294 self.emit("package-selection-changed")
295
296 """
297 Check whether the item at item_path is included or not
298 """
299 def path_included(self, item_path):
300 return self[item_path][self.COL_INC]
301
302 """
303 Add this item, and any of its dependencies, to the image contents
304 """
305 def include_item(self, item_path, binb=""):
306 if self.path_included(item_path):
307 return
308
309 item_name = self[item_path][self.COL_NAME]
310 item_deps = self[item_path][self.COL_RDEP]
311
312 self[item_path][self.COL_INC] = True
313
314 item_bin = self[item_path][self.COL_BINB].split(', ')
315 if binb and not binb in item_bin:
316 item_bin.append(binb)
317 self[item_path][self.COL_BINB] = ', '.join(item_bin).lstrip(', ')
318
319 if item_deps:
320 # Ensure all of the items deps are included and, where appropriate,
321 # add this item to their COL_BINB
322 for dep in item_deps.split(" "):
323 if dep.startswith('('):
324 continue
325 # If the contents model doesn't already contain dep, add it
326 dep_path = self.find_path_for_item(dep)
327 if not dep_path:
328 continue
329 dep_included = self.path_included(dep_path)
330
331 if dep_included and not dep in item_bin:
332 # don't set the COL_BINB to this item if the target is an
333 # item in our own COL_BINB
334 dep_bin = self[dep_path][self.COL_BINB].split(', ')
335 if not item_name in dep_bin:
336 dep_bin.append(item_name)
337 self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ')
338 elif not dep_included:
339 self.include_item(dep_path, binb=item_name)
340
341 def exclude_item(self, item_path):
342 if not self.path_included(item_path):
343 return
344
345 self[item_path][self.COL_INC] = False
346
347 item_name = self[item_path][self.COL_NAME]
348 item_deps = self[item_path][self.COL_RDEP]
349 if item_deps:
350 for dep in item_deps.split(" "):
351 if dep.startswith('('):
352 continue
353 dep_path = self.find_path_for_item(dep)
354 if not dep_path:
355 continue
356 dep_bin = self[dep_path][self.COL_BINB].split(', ')
357 if item_name in dep_bin:
358 dep_bin.remove(item_name)
359 self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ')
360
361 item_bin = self[item_path][self.COL_BINB].split(', ')
362 if item_bin:
363 for binb in item_bin:
364 binb_path = self.find_path_for_item(binb)
365 if not binb_path:
366 continue
367 self.exclude_item(binb_path)
368
369 """
370 Empty self.contents by setting the include of each entry to None
371 """
372 def reset(self):
373 it = self.get_iter_first()
374 while it:
375 self.set(it,
376 self.COL_INC, False,
377 self.COL_BINB, "")
378 it = self.iter_next(it)
379
380 self.selection_change_notification()
381
382 def get_selected_packages(self):
383 packagelist = []
384
385 it = self.get_iter_first()
386 while it:
387 if self.get_value(it, self.COL_INC):
388 name = self.get_value(it, self.COL_NAME)
389 packagelist.append(name)
390 it = self.iter_next(it)
391
392 return packagelist
393
394 def get_user_selected_packages(self):
395 packagelist = []
396
397 it = self.get_iter_first()
398 while it:
399 if self.get_value(it, self.COL_INC):
400 binb = self.get_value(it, self.COL_BINB)
401 if binb == "User Selected":
402 name = self.get_value(it, self.COL_NAME)
403 packagelist.append(name)
404 it = self.iter_next(it)
405
406 return packagelist
407
408 def get_selected_packages_toolchain(self):
409 packagelist = []
410
411 it = self.get_iter_first()
412 while it:
413 if self.get_value(it, self.COL_INC):
414 name = self.get_value(it, self.COL_NAME)
415 if name.endswith("-dev") or name.endswith("-dbg"):
416 packagelist.append(name)
417 it = self.iter_next(it)
418
419 return list(set(packagelist + self.__toolchain_required_packages__));
420
421 """
422 Package model may be incomplete, therefore when calling the
423 set_selected_packages(), some packages will not be set included.
424 Return the un-set packages list.
425 """
426 def set_selected_packages(self, packagelist, user_selected=False):
427 left = []
428 binb = 'User Selected' if user_selected else ''
429 for pn in packagelist:
430 if pn in self.pn_path.keys():
431 path = self.pn_path[pn]
432 self.include_item(item_path=path, binb=binb)
433 else:
434 left.append(pn)
435
436 self.selection_change_notification()
437 return left
438
439 """
440 Return the selected package size, unit is B.
441 """
442 def get_packages_size(self):
443 packages_size = 0
444 it = self.get_iter_first()
445 while it:
446 if self.get_value(it, self.COL_INC):
447 str_size = self.get_value(it, self.COL_SIZE)
448 if not str_size:
449 continue
450
451 packages_size += HobPage._string_to_size(str_size)
452
453 it = self.iter_next(it)
454 return packages_size
455
456 """
457 Resync the state of included items to a backup column before performing the fadeout visible effect
458 """
459 def resync_fadeout_column(self, model_first_iter=None):
460 it = model_first_iter
461 while it:
462 active = self.get_value(it, self.COL_INC)
463 self.set(it, self.COL_FADE_INC, active)
464 it = self.iter_next(it)
465
466#
467# RecipeListModel
468#
469class RecipeListModel(gtk.ListStore):
470 """
471 This class defines an gtk.ListStore subclass which will convert the output
472 of the bb.event.TargetsTreeGenerated event into a gtk.ListStore whilst also
473 providing convenience functions to access gtk.TreeModel subclasses which
474 provide filtered views of the data.
475 """
476 (COL_NAME, COL_DESC, COL_LIC, COL_GROUP, COL_DEPS, COL_BINB, COL_TYPE, COL_INC, COL_IMG, COL_INSTALL, COL_PN, COL_FADE_INC, COL_SUMMARY, COL_VERSION,
477 COL_REVISION, COL_HOMEPAGE, COL_BUGTRACKER, COL_FILE) = range(18)
478
479 __custom_image__ = "Start with an empty image recipe"
480
481 __gsignals__ = {
482 "recipe-selection-changed" : (gobject.SIGNAL_RUN_LAST,
483 gobject.TYPE_NONE,
484 ()),
485 }
486
487 """
488 """
489 def __init__(self):
490 gtk.ListStore.__init__ (self,
491 gobject.TYPE_STRING,
492 gobject.TYPE_STRING,
493 gobject.TYPE_STRING,
494 gobject.TYPE_STRING,
495 gobject.TYPE_STRING,
496 gobject.TYPE_STRING,
497 gobject.TYPE_STRING,
498 gobject.TYPE_BOOLEAN,
499 gobject.TYPE_BOOLEAN,
500 gobject.TYPE_STRING,
501 gobject.TYPE_STRING,
502 gobject.TYPE_BOOLEAN,
503 gobject.TYPE_STRING,
504 gobject.TYPE_STRING,
505 gobject.TYPE_STRING,
506 gobject.TYPE_STRING,
507 gobject.TYPE_STRING,
508 gobject.TYPE_STRING)
509 self.sort_column_id, self.sort_order = RecipeListModel.COL_NAME, gtk.SORT_ASCENDING
510
511 """
512 Find the model path for the item_name
513 Returns the path in the model or None
514 """
515 def find_path_for_item(self, item_name):
516 if self.non_target_name(item_name) or item_name not in self.pn_path.keys():
517 return None
518 else:
519 return self.pn_path[item_name]
520
521 def find_item_for_path(self, item_path):
522 return self[item_path][self.COL_NAME]
523
524 """
525 Helper method to determine whether name is a target pn
526 """
527 def non_target_name(self, name):
528 if name and ('-native' in name):
529 return True
530 return False
531
532 """
533 Helper function to determine whether an item is an item specified by filter
534 """
535 def tree_model_filter(self, model, it, filter):
536 name = model.get_value(it, self.COL_NAME)
537 if self.non_target_name(name):
538 return False
539
540 for key in filter.keys():
541 if key == self.COL_NAME:
542 if filter[key] != 'Search recipes by name' and filter[key] != 'Search package groups by name':
543 if filter[key] not in name:
544 return False
545 else:
546 if model.get_value(it, key) not in filter[key]:
547 return False
548 self.filtered_nb += 1
549
550 return True
551
552 def exclude_item_sort_func(self, model, iter1, iter2, user_data=None):
553 if user_data:
554 val1 = model.get_value(iter1, RecipeListModel.COL_NAME)
555 val2 = model.get_value(iter2, RecipeListModel.COL_NAME)
556 return self.cmp_vals(val1, val2, user_data)
557 else:
558 val1 = model.get_value(iter1, RecipeListModel.COL_FADE_INC)
559 val2 = model.get_value(iter2, RecipeListModel.COL_INC)
560 return ((val1 == True) and (val2 == False))
561
562 def include_item_sort_func(self, model, iter1, iter2, user_data=None):
563 if user_data:
564 val1 = model.get_value(iter1, RecipeListModel.COL_NAME)
565 val2 = model.get_value(iter2, RecipeListModel.COL_NAME)
566 return self.cmp_vals(val1, val2, user_data)
567 else:
568 val1 = model.get_value(iter1, RecipeListModel.COL_INC)
569 val2 = model.get_value(iter2, RecipeListModel.COL_INC)
570 return ((val1 == False) and (val2 == True))
571
572 def sort_func(self, model, iter1, iter2, user_data):
573 val1 = model.get_value(iter1, RecipeListModel.COL_NAME)
574 val2 = model.get_value(iter2, RecipeListModel.COL_NAME)
575 return self.cmp_vals(val1, val2, user_data)
576
577 def cmp_vals(self, val1, val2, user_data):
578 if val1.startswith(user_data) and not val2.startswith(user_data):
579 return -1
580 elif not val1.startswith(user_data) and val2.startswith(user_data):
581 return 1
582 else:
583 return cmp(val1, val2)
584
585 """
586 Create, if required, and return a filtered gtk.TreeModelSort
587 containing only the items specified by filter
588 """
589 def tree_model(self, filter, excluded_items_ahead=False, included_items_ahead=False, search_data=None, initial=False):
590 model = self.filter_new()
591 self.filtered_nb = 0
592 model.set_visible_func(self.tree_model_filter, filter)
593
594 sort = gtk.TreeModelSort(model)
595 sort.connect ('sort-column-changed', self.sort_column_changed_cb)
596 if initial:
597 sort.set_sort_column_id(RecipeListModel.COL_NAME, gtk.SORT_ASCENDING)
598 sort.set_default_sort_func(None)
599 elif excluded_items_ahead:
600 sort.set_default_sort_func(self.exclude_item_sort_func, search_data)
601 elif included_items_ahead:
602 sort.set_default_sort_func(self.include_item_sort_func, search_data)
603 else:
604 if search_data and search_data!='Search recipes by name' and search_data!='Search package groups by name':
605 sort.set_default_sort_func(self.sort_func, search_data)
606 else:
607 sort.set_sort_column_id(self.sort_column_id, self.sort_order)
608 sort.set_default_sort_func(None)
609
610 sort.set_sort_func(RecipeListModel.COL_INC, self.sort_column, RecipeListModel.COL_INC)
611 sort.set_sort_func(RecipeListModel.COL_GROUP, self.sort_column, RecipeListModel.COL_GROUP)
612 sort.set_sort_func(RecipeListModel.COL_BINB, self.sort_binb_column)
613 sort.set_sort_func(RecipeListModel.COL_LIC, self.sort_column, RecipeListModel.COL_LIC)
614 return sort
615
616 def sort_column_changed_cb (self, data):
617 self.sort_column_id, self.sort_order = data.get_sort_column_id ()
618
619 def sort_column(self, model, row1, row2, col):
620 value1 = model.get_value(row1, col)
621 value2 = model.get_value(row2, col)
622 cmp_res = cmp(value1, value2)
623 if cmp_res!=0:
624 if col==RecipeListModel.COL_INC:
625 return -cmp_res
626 else:
627 return cmp_res
628 else:
629 name1 = model.get_value(row1, RecipeListModel.COL_NAME)
630 name2 = model.get_value(row2, RecipeListModel.COL_NAME)
631 return cmp(name1,name2)
632
633 def sort_binb_column(self, model, row1, row2):
634 value1 = model.get_value(row1, RecipeListModel.COL_BINB)
635 value2 = model.get_value(row2, RecipeListModel.COL_BINB)
636 value1_list = value1.split(', ')
637 value2_list = value2.split(', ')
638
639 value1 = value1_list[0]
640 value2 = value2_list[0]
641
642 cmp_res = cmp(value1, value2)
643 if cmp_res==0:
644 cmp_size = cmp(len(value1_list), len(value2_list))
645 if cmp_size==0:
646 name1 = model.get_value(row1, RecipeListModel.COL_NAME)
647 name2 = model.get_value(row2, RecipeListModel.COL_NAME)
648 return cmp(name1,name2)
649 else:
650 return cmp_size
651 else:
652 return cmp_res
653
654 def convert_vpath_to_path(self, view_model, view_path):
655 filtered_model_path = view_model.convert_path_to_child_path(view_path)
656 filtered_model = view_model.get_model()
657
658 # get the path of the original model
659 path = filtered_model.convert_path_to_child_path(filtered_model_path)
660 return path
661
662 def convert_path_to_vpath(self, view_model, path):
663 it = view_model.get_iter_first()
664 while it:
665 name = self.find_item_for_path(path)
666 view_name = view_model.get_value(it, RecipeListModel.COL_NAME)
667 if view_name == name:
668 view_path = view_model.get_path(it)
669 return view_path
670 it = view_model.iter_next(it)
671 return None
672
673 """
674 The populate() function takes as input the data from a
675 bb.event.TargetsTreeGenerated event and populates the RecipeList.
676 """
677 def populate(self, event_model):
678 # First clear the model, in case repopulating
679 self.clear()
680
681 # dummy image for prompt
682 self.set_in_list(self.__custom_image__, "Use 'Edit image recipe' to customize recipes and packages " \
683 "to be included in your image ")
684
685 for item in event_model["pn"]:
686 name = item
687 desc = event_model["pn"][item]["description"]
688 lic = event_model["pn"][item]["license"]
689 group = event_model["pn"][item]["section"]
690 inherits = event_model["pn"][item]["inherits"]
691 summary = event_model["pn"][item]["summary"]
692 version = event_model["pn"][item]["version"]
693 revision = event_model["pn"][item]["prevision"]
694 homepage = event_model["pn"][item]["homepage"]
695 bugtracker = event_model["pn"][item]["bugtracker"]
696 filename = event_model["pn"][item]["filename"]
697 install = []
698
699 depends = event_model["depends"].get(item, []) + event_model["rdepends-pn"].get(item, [])
700
701 if ('packagegroup.bbclass' in " ".join(inherits)):
702 atype = 'packagegroup'
703 elif ('image.bbclass' in " ".join(inherits)):
704 if name != "hob-image":
705 atype = 'image'
706 install = event_model["rdepends-pkg"].get(item, []) + event_model["rrecs-pkg"].get(item, [])
707 elif ('meta-' in name):
708 atype = 'toolchain'
709 elif (name == 'dummy-image' or name == 'dummy-toolchain'):
710 atype = 'dummy'
711 else:
712 atype = 'recipe'
713
714 self.set(self.append(), self.COL_NAME, item, self.COL_DESC, desc,
715 self.COL_LIC, lic, self.COL_GROUP, group,
716 self.COL_DEPS, " ".join(depends), self.COL_BINB, "",
717 self.COL_TYPE, atype, self.COL_INC, False,
718 self.COL_IMG, False, self.COL_INSTALL, " ".join(install), self.COL_PN, item,
719 self.COL_SUMMARY, summary, self.COL_VERSION, version, self.COL_REVISION, revision,
720 self.COL_HOMEPAGE, homepage, self.COL_BUGTRACKER, bugtracker,
721 self.COL_FILE, filename)
722
723 self.pn_path = {}
724 it = self.get_iter_first()
725 while it:
726 pn = self.get_value(it, self.COL_NAME)
727 path = self.get_path(it)
728 self.pn_path[pn] = path
729 it = self.iter_next(it)
730
731 def set_in_list(self, item, desc):
732 self.set(self.append(), self.COL_NAME, item,
733 self.COL_DESC, desc,
734 self.COL_LIC, "", self.COL_GROUP, "",
735 self.COL_DEPS, "", self.COL_BINB, "",
736 self.COL_TYPE, "image", self.COL_INC, False,
737 self.COL_IMG, False, self.COL_INSTALL, "", self.COL_PN, item,
738 self.COL_SUMMARY, "", self.COL_VERSION, "", self.COL_REVISION, "",
739 self.COL_HOMEPAGE, "", self.COL_BUGTRACKER, "")
740 self.pn_path = {}
741 it = self.get_iter_first()
742 while it:
743 pn = self.get_value(it, self.COL_NAME)
744 path = self.get_path(it)
745 self.pn_path[pn] = path
746 it = self.iter_next(it)
747
748 """
749 Update the model, send out the notification.
750 """
751 def selection_change_notification(self):
752 self.emit("recipe-selection-changed")
753
754 def path_included(self, item_path):
755 return self[item_path][self.COL_INC]
756
757 """
758 Add this item, and any of its dependencies, to the image contents
759 """
760 def include_item(self, item_path, binb="", image_contents=False):
761 if self.path_included(item_path):
762 return
763
764 item_name = self[item_path][self.COL_NAME]
765 item_deps = self[item_path][self.COL_DEPS]
766
767 self[item_path][self.COL_INC] = True
768
769 item_bin = self[item_path][self.COL_BINB].split(', ')
770 if binb and not binb in item_bin:
771 item_bin.append(binb)
772 self[item_path][self.COL_BINB] = ', '.join(item_bin).lstrip(', ')
773
774 # We want to do some magic with things which are brought in by the
775 # base image so tag them as so
776 if image_contents:
777 self[item_path][self.COL_IMG] = True
778
779 if item_deps:
780 # Ensure all of the items deps are included and, where appropriate,
781 # add this item to their COL_BINB
782 for dep in item_deps.split(" "):
783 # If the contents model doesn't already contain dep, add it
784 dep_path = self.find_path_for_item(dep)
785 if not dep_path:
786 continue
787 dep_included = self.path_included(dep_path)
788
789 if dep_included and not dep in item_bin:
790 # don't set the COL_BINB to this item if the target is an
791 # item in our own COL_BINB
792 dep_bin = self[dep_path][self.COL_BINB].split(', ')
793 if not item_name in dep_bin:
794 dep_bin.append(item_name)
795 self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ')
796 elif not dep_included:
797 self.include_item(dep_path, binb=item_name, image_contents=image_contents)
798 dep_bin = self[item_path][self.COL_BINB].split(', ')
799 if self[item_path][self.COL_NAME] in dep_bin:
800 dep_bin.remove(self[item_path][self.COL_NAME])
801 self[item_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ')
802
803 def exclude_item(self, item_path):
804 if not self.path_included(item_path):
805 return
806
807 self[item_path][self.COL_INC] = False
808
809 item_name = self[item_path][self.COL_NAME]
810 item_deps = self[item_path][self.COL_DEPS]
811 if item_deps:
812 for dep in item_deps.split(" "):
813 dep_path = self.find_path_for_item(dep)
814 if not dep_path:
815 continue
816 dep_bin = self[dep_path][self.COL_BINB].split(', ')
817 if item_name in dep_bin:
818 dep_bin.remove(item_name)
819 self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ')
820
821 item_bin = self[item_path][self.COL_BINB].split(', ')
822 if item_bin:
823 for binb in item_bin:
824 binb_path = self.find_path_for_item(binb)
825 if not binb_path:
826 continue
827 self.exclude_item(binb_path)
828
829 def reset(self):
830 it = self.get_iter_first()
831 while it:
832 self.set(it,
833 self.COL_INC, False,
834 self.COL_BINB, "",
835 self.COL_IMG, False)
836 it = self.iter_next(it)
837
838 self.selection_change_notification()
839
840 """
841 Returns two lists. One of user selected recipes and the other containing
842 all selected recipes
843 """
844 def get_selected_recipes(self):
845 allrecipes = []
846 userrecipes = []
847
848 it = self.get_iter_first()
849 while it:
850 if self.get_value(it, self.COL_INC):
851 name = self.get_value(it, self.COL_PN)
852 type = self.get_value(it, self.COL_TYPE)
853 if type != "image":
854 allrecipes.append(name)
855 sel = "User Selected" in self.get_value(it, self.COL_BINB)
856 if sel:
857 userrecipes.append(name)
858 it = self.iter_next(it)
859
860 return list(set(userrecipes)), list(set(allrecipes))
861
862 def set_selected_recipes(self, recipelist):
863 for pn in recipelist:
864 if pn in self.pn_path.keys():
865 path = self.pn_path[pn]
866 self.include_item(item_path=path,
867 binb="User Selected")
868 self.selection_change_notification()
869
870 def get_selected_image(self):
871 it = self.get_iter_first()
872 while it:
873 if self.get_value(it, self.COL_INC):
874 name = self.get_value(it, self.COL_PN)
875 type = self.get_value(it, self.COL_TYPE)
876 if type == "image":
877 sel = "User Selected" in self.get_value(it, self.COL_BINB)
878 if sel:
879 return name
880 it = self.iter_next(it)
881 return None
882
883 def set_selected_image(self, img):
884 if not img:
885 return
886 self.reset()
887 path = self.find_path_for_item(img)
888 self.include_item(item_path=path,
889 binb="User Selected",
890 image_contents=True)
891 self.selection_change_notification()
892
893 def set_custom_image_version(self, version):
894 self.custom_image_version = version
895
896 def get_custom_image_version(self):
897 return self.custom_image_version
898
899 def is_custom_image(self):
900 return self.get_selected_image() == self.__custom_image__
diff --git a/bitbake/lib/bb/ui/crumbs/hobpages.py b/bitbake/lib/bb/ui/crumbs/hobpages.py
new file mode 100755
index 0000000000..0fd3598c3a
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hobpages.py
@@ -0,0 +1,128 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2012 Intel Corporation
6#
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24from bb.ui.crumbs.hobcolor import HobColors
25from bb.ui.crumbs.hobwidget import hwc
26
27#
28# HobPage: the super class for all Hob-related pages
29#
30class HobPage (gtk.VBox):
31
32 def __init__(self, builder, title = None):
33 super(HobPage, self).__init__(False, 0)
34 self.builder = builder
35 self.builder_width, self.builder_height = self.builder.size_request()
36
37 if not title:
38 self.title = "Hob -- Image Creator"
39 else:
40 self.title = title
41 self.title_label = gtk.Label()
42
43 self.box_group_area = gtk.VBox(False, 12)
44 self.box_group_area.set_size_request(self.builder_width - 73 - 73, self.builder_height - 88 - 15 - 15)
45 self.group_align = gtk.Alignment(xalign = 0, yalign=0.5, xscale=1, yscale=1)
46 self.group_align.set_padding(15, 15, 73, 73)
47 self.group_align.add(self.box_group_area)
48 self.box_group_area.set_homogeneous(False)
49
50 def set_title(self, title):
51 self.title = title
52 self.title_label.set_markup("<span size='x-large'>%s</span>" % self.title)
53
54 def add_onto_top_bar(self, widget = None, padding = 0):
55 # the top button occupies 1/7 of the page height
56 # setup an event box
57 eventbox = gtk.EventBox()
58 style = eventbox.get_style().copy()
59 style.bg[gtk.STATE_NORMAL] = eventbox.get_colormap().alloc_color(HobColors.LIGHT_GRAY, False, False)
60 eventbox.set_style(style)
61 eventbox.set_size_request(-1, 88)
62
63 hbox = gtk.HBox()
64
65 self.title_label = gtk.Label()
66 self.title_label.set_markup("<span size='x-large'>%s</span>" % self.title)
67 hbox.pack_start(self.title_label, expand=False, fill=False, padding=20)
68
69 if widget:
70 # add the widget in the event box
71 hbox.pack_end(widget, expand=False, fill=False, padding=padding)
72 eventbox.add(hbox)
73
74 return eventbox
75
76 def span_tag(self, size="medium", weight="normal", forground="#1c1c1c"):
77 span_tag = "weight='%s' foreground='%s' size='%s'" % (weight, forground, size)
78 return span_tag
79
80 def append_toolbar_button(self, toolbar, buttonname, icon_disp, icon_hovor, tip, cb):
81 # Create a button and append it on the toolbar according to button name
82 icon = gtk.Image()
83 icon_display = icon_disp
84 icon_hover = icon_hovor
85 pix_buffer = gtk.gdk.pixbuf_new_from_file(icon_display)
86 icon.set_from_pixbuf(pix_buffer)
87 tip_text = tip
88 button = toolbar.append_item(buttonname, tip, None, icon, cb)
89 return button
90
91 @staticmethod
92 def _size_to_string(size):
93 try:
94 if not size:
95 size_str = "0 B"
96 else:
97 if len(str(int(size))) > 6:
98 size_str = '%.1f' % (size*1.0/(1024*1024)) + ' MB'
99 elif len(str(int(size))) > 3:
100 size_str = '%.1f' % (size*1.0/1024) + ' KB'
101 else:
102 size_str = str(size) + ' B'
103 except:
104 size_str = "0 B"
105 return size_str
106
107 @staticmethod
108 def _string_to_size(str_size):
109 try:
110 if not str_size:
111 size = 0
112 else:
113 unit = str_size.split()
114 if len(unit) > 1:
115 if unit[1] == 'MB':
116 size = float(unit[0])*1024*1024
117 elif unit[1] == 'KB':
118 size = float(unit[0])*1024
119 elif unit[1] == 'B':
120 size = float(unit[0])
121 else:
122 size = 0
123 else:
124 size = float(unit[0])
125 except:
126 size = 0
127 return size
128
diff --git a/bitbake/lib/bb/ui/crumbs/hobwidget.py b/bitbake/lib/bb/ui/crumbs/hobwidget.py
new file mode 100644
index 0000000000..3707d6160d
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/hobwidget.py
@@ -0,0 +1,904 @@
1# BitBake Graphical GTK User Interface
2#
3# Copyright (C) 2011-2012 Intel Corporation
4#
5# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
6# Authored by Shane Wang <shane.wang@intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20import gtk
21import gobject
22import os
23import os.path
24import sys
25import pango, pangocairo
26import cairo
27import math
28
29from bb.ui.crumbs.hobcolor import HobColors
30from bb.ui.crumbs.persistenttooltip import PersistentTooltip
31
32class hwc:
33
34 MAIN_WIN_WIDTH = 1024
35 MAIN_WIN_HEIGHT = 700
36
37class hic:
38
39 HOB_ICON_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ("ui/icons/"))
40
41 ICON_RCIPE_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('recipe/recipe_display.png'))
42 ICON_RCIPE_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('recipe/recipe_hover.png'))
43 ICON_PACKAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('packages/packages_display.png'))
44 ICON_PACKAGES_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('packages/packages_hover.png'))
45 ICON_LAYERS_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('layers/layers_display.png'))
46 ICON_LAYERS_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('layers/layers_hover.png'))
47 ICON_IMAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('images/images_display.png'))
48 ICON_IMAGES_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('images/images_hover.png'))
49 ICON_SETTINGS_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('settings/settings_display.png'))
50 ICON_SETTINGS_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('settings/settings_hover.png'))
51 ICON_INFO_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_display.png'))
52 ICON_INFO_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_hover.png'))
53 ICON_INDI_CONFIRM_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/confirmation.png'))
54 ICON_INDI_ERROR_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/denied.png'))
55 ICON_INDI_REMOVE_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/remove.png'))
56 ICON_INDI_REMOVE_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/remove-hover.png'))
57 ICON_INDI_ADD_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/add.png'))
58 ICON_INDI_ADD_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/add-hover.png'))
59 ICON_INDI_REFRESH_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/refresh.png'))
60 ICON_INDI_ALERT_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/alert.png'))
61 ICON_INDI_TICK_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/tick.png'))
62 ICON_INDI_INFO_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/info.png'))
63
64class HobViewTable (gtk.VBox):
65 """
66 A VBox to contain the table for different recipe views and package view
67 """
68 __gsignals__ = {
69 "toggled" : (gobject.SIGNAL_RUN_LAST,
70 gobject.TYPE_NONE,
71 (gobject.TYPE_PYOBJECT,
72 gobject.TYPE_STRING,
73 gobject.TYPE_INT,
74 gobject.TYPE_PYOBJECT,)),
75 "row-activated" : (gobject.SIGNAL_RUN_LAST,
76 gobject.TYPE_NONE,
77 (gobject.TYPE_PYOBJECT,
78 gobject.TYPE_PYOBJECT,)),
79 "cell-fadeinout-stopped" : (gobject.SIGNAL_RUN_LAST,
80 gobject.TYPE_NONE,
81 (gobject.TYPE_PYOBJECT,
82 gobject.TYPE_PYOBJECT,
83 gobject.TYPE_PYOBJECT,)),
84 }
85
86 def __init__(self, columns, name):
87 gtk.VBox.__init__(self, False, 6)
88 self.table_tree = gtk.TreeView()
89 self.table_tree.set_headers_visible(True)
90 self.table_tree.set_headers_clickable(True)
91 self.table_tree.set_rules_hint(True)
92 self.table_tree.set_enable_tree_lines(True)
93 self.table_tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
94 self.toggle_columns = []
95 self.table_tree.connect("row-activated", self.row_activated_cb)
96 self.top_bar = None
97 self.tab_name = name
98
99 for i, column in enumerate(columns):
100 col_name = column['col_name']
101 col = gtk.TreeViewColumn(col_name)
102 col.set_clickable(True)
103 col.set_resizable(True)
104 if self.tab_name.startswith('Included'):
105 if col_name!='Included':
106 col.set_sort_column_id(column['col_id'])
107 else:
108 col.set_sort_column_id(column['col_id'])
109 if 'col_min' in column.keys():
110 col.set_min_width(column['col_min'])
111 if 'col_max' in column.keys():
112 col.set_max_width(column['col_max'])
113 if 'expand' in column.keys():
114 col.set_expand(True)
115 self.table_tree.append_column(col)
116
117 if (not 'col_style' in column.keys()) or column['col_style'] == 'text':
118 cell = gtk.CellRendererText()
119 col.pack_start(cell, True)
120 col.set_attributes(cell, text=column['col_id'])
121 if 'col_t_id' in column.keys():
122 col.add_attribute(cell, 'font', column['col_t_id'])
123 elif column['col_style'] == 'check toggle':
124 cell = HobCellRendererToggle()
125 cell.set_property('activatable', True)
126 cell.connect("toggled", self.toggled_cb, i, self.table_tree)
127 cell.connect_render_state_changed(self.stop_cell_fadeinout_cb, self.table_tree)
128 self.toggle_id = i
129 col.pack_end(cell, True)
130 col.set_attributes(cell, active=column['col_id'])
131 self.toggle_columns.append(col_name)
132 if 'col_group' in column.keys():
133 col.set_cell_data_func(cell, self.set_group_number_cb)
134 elif column['col_style'] == 'radio toggle':
135 cell = gtk.CellRendererToggle()
136 cell.set_property('activatable', True)
137 cell.set_radio(True)
138 cell.connect("toggled", self.toggled_cb, i, self.table_tree)
139 self.toggle_id = i
140 col.pack_end(cell, True)
141 col.set_attributes(cell, active=column['col_id'])
142 self.toggle_columns.append(col_name)
143 elif column['col_style'] == 'binb':
144 cell = gtk.CellRendererText()
145 col.pack_start(cell, True)
146 col.set_cell_data_func(cell, self.display_binb_cb, column['col_id'])
147 if 'col_t_id' in column.keys():
148 col.add_attribute(cell, 'font', column['col_t_id'])
149
150 self.scroll = gtk.ScrolledWindow()
151 self.scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
152 self.scroll.add(self.table_tree)
153
154 self.pack_end(self.scroll, True, True, 0)
155
156 def add_no_result_bar(self, entry):
157 color = HobColors.KHAKI
158 self.top_bar = gtk.EventBox()
159 self.top_bar.set_size_request(-1, 70)
160 self.top_bar.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
161 self.top_bar.set_flags(gtk.CAN_DEFAULT)
162 self.top_bar.grab_default()
163
164 no_result_tab = gtk.Table(5, 20, True)
165 self.top_bar.add(no_result_tab)
166
167 label = gtk.Label()
168 label.set_alignment(0.0, 0.5)
169 title = "No results matching your search"
170 label.set_markup("<span size='x-large'><b>%s</b></span>" % title)
171 no_result_tab.attach(label, 1, 14, 1, 4)
172
173 clear_button = HobButton("Clear search")
174 clear_button.set_tooltip_text("Clear search query")
175 clear_button.connect('clicked', self.set_search_entry_clear_cb, entry)
176 no_result_tab.attach(clear_button, 16, 19, 1, 4)
177
178 self.pack_start(self.top_bar, False, True, 12)
179 self.top_bar.show_all()
180
181 def set_search_entry_clear_cb(self, button, search):
182 if search.get_editable() == True:
183 search.set_text("")
184 search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False)
185 search.grab_focus()
186
187 def display_binb_cb(self, col, cell, model, it, col_id):
188 binb = model.get_value(it, col_id)
189 # Just display the first item
190 if binb:
191 bin = binb.split(', ')
192 total_no = len(bin)
193 if total_no > 1 and bin[0] == "User Selected":
194 if total_no > 2:
195 present_binb = bin[1] + ' (+' + str(total_no - 1) + ')'
196 else:
197 present_binb = bin[1]
198 else:
199 if total_no > 1:
200 present_binb = bin[0] + ' (+' + str(total_no - 1) + ')'
201 else:
202 present_binb = bin[0]
203 cell.set_property('text', present_binb)
204 else:
205 cell.set_property('text', "")
206 return True
207
208 def set_model(self, tree_model):
209 self.table_tree.set_model(tree_model)
210
211 def toggle_default(self):
212 model = self.table_tree.get_model()
213 if not model:
214 return
215 iter = model.get_iter_first()
216 if iter:
217 rowpath = model.get_path(iter)
218 model[rowpath][self.toggle_id] = True
219
220 def toggled_cb(self, cell, path, columnid, tree):
221 self.emit("toggled", cell, path, columnid, tree)
222
223 def row_activated_cb(self, tree, path, view_column):
224 if not view_column.get_title() in self.toggle_columns:
225 self.emit("row-activated", tree.get_model(), path)
226
227 def stop_cell_fadeinout_cb(self, ctrl, cell, tree):
228 self.emit("cell-fadeinout-stopped", ctrl, cell, tree)
229
230 def set_group_number_cb(self, col, cell, model, iter):
231 if model and (model.iter_parent(iter) == None):
232 cell.cell_attr["number_of_children"] = model.iter_n_children(iter)
233 else:
234 cell.cell_attr["number_of_children"] = 0
235
236 def connect_group_selection(self, cb_func):
237 self.table_tree.get_selection().connect("changed", cb_func)
238
239"""
240A method to calculate a softened value for the colour of widget when in the
241provided state.
242
243widget: the widget whose style to use
244state: the state of the widget to use the style for
245
246Returns a string value representing the softened colour
247"""
248def soften_color(widget, state=gtk.STATE_NORMAL):
249 # this colour munging routine is heavily inspired bu gdu_util_get_mix_color()
250 # from gnome-disk-utility:
251 # http://git.gnome.org/browse/gnome-disk-utility/tree/src/gdu-gtk/gdu-gtk.c?h=gnome-3-0
252 blend = 0.7
253 style = widget.get_style()
254 color = style.text[state]
255 color.red = color.red * blend + style.base[state].red * (1.0 - blend)
256 color.green = color.green * blend + style.base[state].green * (1.0 - blend)
257 color.blue = color.blue * blend + style.base[state].blue * (1.0 - blend)
258 return color.to_string()
259
260class BaseHobButton(gtk.Button):
261 """
262 A gtk.Button subclass which follows the visual design of Hob for primary
263 action buttons
264
265 label: the text to display as the button's label
266 """
267 def __init__(self, label):
268 gtk.Button.__init__(self, label)
269 HobButton.style_button(self)
270
271 @staticmethod
272 def style_button(button):
273 style = button.get_style()
274 style = gtk.rc_get_style_by_paths(gtk.settings_get_default(), 'gtk-button', 'gtk-button', gobject.TYPE_NONE)
275
276 button.set_flags(gtk.CAN_DEFAULT)
277 button.grab_default()
278
279# label = "<span size='x-large'><b>%s</b></span>" % gobject.markup_escape_text(button.get_label())
280 label = button.get_label()
281 button.set_label(label)
282 button.child.set_use_markup(True)
283
284class HobButton(BaseHobButton):
285 """
286 A gtk.Button subclass which follows the visual design of Hob for primary
287 action buttons
288
289 label: the text to display as the button's label
290 """
291 def __init__(self, label):
292 BaseHobButton.__init__(self, label)
293 HobButton.style_button(self)
294
295class HobAltButton(BaseHobButton):
296 """
297 A gtk.Button subclass which has no relief, and so is more discrete
298 """
299 def __init__(self, label):
300 BaseHobButton.__init__(self, label)
301 HobAltButton.style_button(self)
302
303 """
304 A callback for the state-changed event to ensure the text is displayed
305 differently when the widget is not sensitive
306 """
307 @staticmethod
308 def desensitise_on_state_change_cb(button, state):
309 if not button.get_property("sensitive"):
310 HobAltButton.set_text(button, False)
311 else:
312 HobAltButton.set_text(button, True)
313
314 """
315 Set the button label with an appropriate colour for the current widget state
316 """
317 @staticmethod
318 def set_text(button, sensitive=True):
319 if sensitive:
320 colour = HobColors.PALE_BLUE
321 else:
322 colour = HobColors.LIGHT_GRAY
323 button.set_label("<span size='large' color='%s'><b>%s</b></span>" % (colour, gobject.markup_escape_text(button.text)))
324 button.child.set_use_markup(True)
325
326class HobImageButton(gtk.Button):
327 """
328 A gtk.Button with an icon and two rows of text, the second of which is
329 displayed in a blended colour.
330
331 primary_text: the main button label
332 secondary_text: optional second line of text
333 icon_path: path to the icon file to display on the button
334 """
335 def __init__(self, primary_text, secondary_text="", icon_path="", hover_icon_path=""):
336 gtk.Button.__init__(self)
337 self.set_relief(gtk.RELIEF_NONE)
338
339 self.icon_path = icon_path
340 self.hover_icon_path = hover_icon_path
341
342 hbox = gtk.HBox(False, 10)
343 hbox.show()
344 self.add(hbox)
345 self.icon = gtk.Image()
346 self.icon.set_from_file(self.icon_path)
347 self.icon.set_alignment(0.5, 0.0)
348 self.icon.show()
349 if self.hover_icon_path and len(self.hover_icon_path):
350 self.connect("enter-notify-event", self.set_hover_icon_cb)
351 self.connect("leave-notify-event", self.set_icon_cb)
352 hbox.pack_start(self.icon, False, False, 0)
353 label = gtk.Label()
354 label.set_alignment(0.0, 0.5)
355 colour = soften_color(label)
356 mark = "<span size='x-large'>%s</span>\n<span size='medium' fgcolor='%s' weight='ultralight'>%s</span>" % (primary_text, colour, secondary_text)
357 label.set_markup(mark)
358 label.show()
359 hbox.pack_start(label, True, True, 0)
360
361 def set_hover_icon_cb(self, widget, event):
362 self.icon.set_from_file(self.hover_icon_path)
363
364 def set_icon_cb(self, widget, event):
365 self.icon.set_from_file(self.icon_path)
366
367class HobInfoButton(gtk.EventBox):
368 """
369 This class implements a button-like widget per the Hob visual and UX designs
370 which will display a persistent tooltip, with the contents of tip_markup, when
371 clicked.
372
373 tip_markup: the Pango Markup to be displayed in the persistent tooltip
374 """
375 def __init__(self, tip_markup, parent=None):
376 gtk.EventBox.__init__(self)
377 self.image = gtk.Image()
378 self.image.set_from_file(
379 hic.ICON_INFO_DISPLAY_FILE)
380 self.image.show()
381 self.add(self.image)
382 self.tip_markup = tip_markup
383 self.my_parent = parent
384
385 self.set_events(gtk.gdk.BUTTON_RELEASE |
386 gtk.gdk.ENTER_NOTIFY_MASK |
387 gtk.gdk.LEAVE_NOTIFY_MASK)
388
389 self.connect("button-release-event", self.button_release_cb)
390 self.connect("enter-notify-event", self.mouse_in_cb)
391 self.connect("leave-notify-event", self.mouse_out_cb)
392
393 """
394 When the mouse click is released emulate a button-click and show the associated
395 PersistentTooltip
396 """
397 def button_release_cb(self, widget, event):
398 from bb.ui.crumbs.hig.propertydialog import PropertyDialog
399 self.dialog = PropertyDialog(title = '',
400 parent = self.my_parent,
401 information = self.tip_markup,
402 flags = gtk.DIALOG_DESTROY_WITH_PARENT
403 | gtk.DIALOG_NO_SEPARATOR)
404
405 button = self.dialog.add_button("Close", gtk.RESPONSE_CANCEL)
406 HobAltButton.style_button(button)
407 button.connect("clicked", lambda w: self.dialog.destroy())
408 self.dialog.show_all()
409 self.dialog.run()
410
411 """
412 Change to the prelight image when the mouse enters the widget
413 """
414 def mouse_in_cb(self, widget, event):
415 self.image.set_from_file(hic.ICON_INFO_HOVER_FILE)
416
417 """
418 Change to the stock image when the mouse enters the widget
419 """
420 def mouse_out_cb(self, widget, event):
421 self.image.set_from_file(hic.ICON_INFO_DISPLAY_FILE)
422
423class HobIndicator(gtk.DrawingArea):
424 def __init__(self, count):
425 gtk.DrawingArea.__init__(self)
426 # Set no window for transparent background
427 self.set_has_window(False)
428 self.set_size_request(38,38)
429 # We need to pass through button clicks
430 self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK)
431
432 self.connect('expose-event', self.expose)
433
434 self.count = count
435 self.color = HobColors.GRAY
436
437 def expose(self, widget, event):
438 if self.count and self.count > 0:
439 ctx = widget.window.cairo_create()
440
441 x, y, w, h = self.allocation
442
443 ctx.set_operator(cairo.OPERATOR_OVER)
444 ctx.set_source_color(gtk.gdk.color_parse(self.color))
445 ctx.translate(w/2, h/2)
446 ctx.arc(x, y, min(w,h)/2 - 2, 0, 2*math.pi)
447 ctx.fill_preserve()
448
449 layout = self.create_pango_layout(str(self.count))
450 textw, texth = layout.get_pixel_size()
451 x = (w/2)-(textw/2) + x
452 y = (h/2) - (texth/2) + y
453 ctx.move_to(x, y)
454 self.window.draw_layout(self.style.light_gc[gtk.STATE_NORMAL], int(x), int(y), layout)
455
456 def set_count(self, count):
457 self.count = count
458
459 def set_active(self, active):
460 if active:
461 self.color = HobColors.DEEP_RED
462 else:
463 self.color = HobColors.GRAY
464
465class HobTabLabel(gtk.HBox):
466 def __init__(self, text, count=0):
467 gtk.HBox.__init__(self, False, 0)
468 self.indicator = HobIndicator(count)
469 self.indicator.show()
470 self.pack_end(self.indicator, False, False)
471 self.lbl = gtk.Label(text)
472 self.lbl.set_alignment(0.0, 0.5)
473 self.lbl.show()
474 self.pack_end(self.lbl, True, True, 6)
475
476 def set_count(self, count):
477 self.indicator.set_count(count)
478
479 def set_active(self, active=True):
480 self.indicator.set_active(active)
481
482class HobNotebook(gtk.Notebook):
483 def __init__(self):
484 gtk.Notebook.__init__(self)
485 self.set_property('homogeneous', True)
486
487 self.pages = []
488
489 self.search = None
490 self.search_focus = False
491 self.page_changed = False
492
493 self.connect("switch-page", self.page_changed_cb)
494
495 self.show_all()
496
497 def page_changed_cb(self, nb, page, page_num):
498 for p, lbl in enumerate(self.pages):
499 if p == page_num:
500 lbl.set_active()
501 else:
502 lbl.set_active(False)
503
504 if self.search:
505 self.page_changed = True
506 self.reset_entry(self.search, page_num)
507
508 def append_page(self, child, tab_label, tab_tooltip=None):
509 label = HobTabLabel(tab_label)
510 if tab_tooltip:
511 label.set_tooltip_text(tab_tooltip)
512 label.set_active(False)
513 self.pages.append(label)
514 gtk.Notebook.append_page(self, child, label)
515
516 def set_entry(self, names, tips):
517 self.search = gtk.Entry()
518 self.search_names = names
519 self.search_tips = tips
520 style = self.search.get_style()
521 style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.GRAY, False, False)
522 self.search.set_style(style)
523 self.search.set_text(names[0])
524 self.search.set_tooltip_text(self.search_tips[0])
525 self.search.props.has_tooltip = True
526
527 self.search.set_editable(False)
528 self.search.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, gtk.STOCK_CLEAR)
529 self.search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False)
530 self.search.connect("icon-release", self.set_search_entry_clear_cb)
531 self.search.set_width_chars(30)
532 self.search.show()
533
534 self.search.connect("focus-in-event", self.set_search_entry_editable_cb)
535 self.search.connect("focus-out-event", self.set_search_entry_reset_cb)
536 self.set_action_widget(self.search, gtk.PACK_END)
537
538 def show_indicator_icon(self, title, number):
539 for child in self.pages:
540 if child.lbl.get_label() == title:
541 child.set_count(number)
542
543 def hide_indicator_icon(self, title):
544 for child in self.pages:
545 if child.lbl.get_label() == title:
546 child.set_count(0)
547
548 def set_search_entry_editable_cb(self, search, event):
549 self.search_focus = True
550 search.set_editable(True)
551 text = search.get_text()
552 if text in self.search_names:
553 search.set_text("")
554 style = self.search.get_style()
555 style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.BLACK, False, False)
556 search.set_style(style)
557
558 def set_search_entry_reset_cb(self, search, event):
559 page_num = self.get_current_page()
560 text = search.get_text()
561 if not text:
562 self.reset_entry(search, page_num)
563
564 def reset_entry(self, entry, page_num):
565 style = entry.get_style()
566 style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.GRAY, False, False)
567 entry.set_style(style)
568 entry.set_text(self.search_names[page_num])
569 entry.set_tooltip_text(self.search_tips[page_num])
570 entry.set_editable(False)
571 entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False)
572
573 def set_search_entry_clear_cb(self, search, icon_pos, event):
574 if search.get_editable() == True:
575 search.set_text("")
576 search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False)
577 search.grab_focus()
578
579 def set_page(self, title):
580 for child in self.pages:
581 if child.lbl.get_label() == title:
582 child.grab_focus()
583 self.set_current_page(self.pages.index(child))
584 return
585
586class HobWarpCellRendererText(gtk.CellRendererText):
587 def __init__(self, col_number):
588 gtk.CellRendererText.__init__(self)
589 self.set_property("wrap-mode", pango.WRAP_WORD_CHAR)
590 self.set_property("wrap-width", 300) # default value wrap width is 300
591 self.col_n = col_number
592
593 def do_render(self, window, widget, background_area, cell_area, expose_area, flags):
594 if widget:
595 self.props.wrap_width = self.get_resized_wrap_width(widget, widget.get_column(self.col_n))
596 return gtk.CellRendererText.do_render(self, window, widget, background_area, cell_area, expose_area, flags)
597
598 def get_resized_wrap_width(self, treeview, column):
599 otherCols = []
600 for col in treeview.get_columns():
601 if col != column:
602 otherCols.append(col)
603 adjwidth = treeview.allocation.width - sum(c.get_width() for c in otherCols)
604 adjwidth -= treeview.style_get_property("horizontal-separator") * 4
605 if self.props.wrap_width == adjwidth or adjwidth <= 0:
606 adjwidth = self.props.wrap_width
607 return adjwidth
608
609gobject.type_register(HobWarpCellRendererText)
610
611class HobIconChecker(hic):
612 def set_hob_icon_to_stock_icon(self, file_path, stock_id=""):
613 try:
614 pixbuf = gtk.gdk.pixbuf_new_from_file(file_path)
615 except Exception, e:
616 return None
617
618 if stock_id and (gtk.icon_factory_lookup_default(stock_id) == None):
619 icon_factory = gtk.IconFactory()
620 icon_factory.add_default()
621 icon_factory.add(stock_id, gtk.IconSet(pixbuf))
622 gtk.stock_add([(stock_id, '_label', 0, 0, '')])
623
624 return icon_factory.lookup(stock_id)
625
626 return None
627
628 """
629 For make hob icon consistently by request, and avoid icon view diff by system or gtk version, we use some 'hob icon' to replace the 'gtk icon'.
630 this function check the stock_id and make hob_id to replaced the gtk_id then return it or ""
631 """
632 def check_stock_icon(self, stock_name=""):
633 HOB_CHECK_STOCK_NAME = {
634 ('hic-dialog-info', 'gtk-dialog-info', 'dialog-info') : self.ICON_INDI_INFO_FILE,
635 ('hic-ok', 'gtk-ok', 'ok') : self.ICON_INDI_TICK_FILE,
636 ('hic-dialog-error', 'gtk-dialog-error', 'dialog-error') : self.ICON_INDI_ERROR_FILE,
637 ('hic-dialog-warning', 'gtk-dialog-warning', 'dialog-warning') : self.ICON_INDI_ALERT_FILE,
638 ('hic-task-refresh', 'gtk-execute', 'execute') : self.ICON_INDI_REFRESH_FILE,
639 }
640 valid_stock_id = stock_name
641 if stock_name:
642 for names, path in HOB_CHECK_STOCK_NAME.iteritems():
643 if stock_name in names:
644 valid_stock_id = names[0]
645 if not gtk.icon_factory_lookup_default(valid_stock_id):
646 self.set_hob_icon_to_stock_icon(path, valid_stock_id)
647
648 return valid_stock_id
649
650class HobCellRendererController(gobject.GObject):
651 (MODE_CYCLE_RUNNING, MODE_ONE_SHORT) = range(2)
652 __gsignals__ = {
653 "run-timer-stopped" : (gobject.SIGNAL_RUN_LAST,
654 gobject.TYPE_NONE,
655 ()),
656 }
657 def __init__(self, runningmode=MODE_CYCLE_RUNNING, is_draw_row=False):
658 gobject.GObject.__init__(self)
659 self.timeout_id = None
660 self.current_angle_pos = 0.0
661 self.step_angle = 0.0
662 self.tree_headers_height = 0
663 self.running_cell_areas = []
664 self.running_mode = runningmode
665 self.is_queue_draw_row_area = is_draw_row
666 self.force_stop_enable = False
667
668 def is_active(self):
669 if self.timeout_id:
670 return True
671 else:
672 return False
673
674 def reset_run(self):
675 self.force_stop()
676 self.running_cell_areas = []
677 self.current_angle_pos = 0.0
678 self.step_angle = 0.0
679
680 ''' time_iterval: (1~1000)ms, which will be as the basic interval count for timer
681 init_usrdata: the current data which related the progress-bar will be at
682 min_usrdata: the range of min of user data
683 max_usrdata: the range of max of user data
684 step: each step which you want to progress
685 Note: the init_usrdata should in the range of from min to max, and max should > min
686 step should < (max - min)
687 '''
688 def start_run(self, time_iterval, init_usrdata, min_usrdata, max_usrdata, step, tree):
689 if (not time_iterval) or (not max_usrdata):
690 return
691 usr_range = (max_usrdata - min_usrdata) * 1.0
692 self.current_angle_pos = (init_usrdata * 1.0) / usr_range
693 self.step_angle = (step * 1) / usr_range
694 self.timeout_id = gobject.timeout_add(int(time_iterval),
695 self.make_image_on_progressing_cb, tree)
696 self.tree_headers_height = self.get_treeview_headers_height(tree)
697 self.force_stop_enable = False
698
699 def force_stop(self):
700 self.emit("run-timer-stopped")
701 self.force_stop_enable = True
702 if self.timeout_id:
703 if gobject.source_remove(self.timeout_id):
704 self.timeout_id = None
705
706 def on_draw_pixbuf_cb(self, pixbuf, cr, x, y, img_width, img_height, do_refresh=True):
707 if pixbuf:
708 r = max(img_width/2, img_height/2)
709 cr.translate(x + r, y + r)
710 if do_refresh:
711 cr.rotate(2 * math.pi * self.current_angle_pos)
712
713 cr.set_source_pixbuf(pixbuf, -img_width/2, -img_height/2)
714 cr.paint()
715
716 def on_draw_fadeinout_cb(self, cr, color, x, y, width, height, do_fadeout=True):
717 if do_fadeout:
718 alpha = self.current_angle_pos * 0.8
719 else:
720 alpha = (1.0 - self.current_angle_pos) * 0.8
721
722 cr.set_source_rgba(color.red, color.green, color.blue, alpha)
723 cr.rectangle(x, y, width, height)
724 cr.fill()
725
726 def get_treeview_headers_height(self, tree):
727 if tree and (tree.get_property("headers-visible") == True):
728 height = tree.get_allocation().height - tree.get_bin_window().get_size()[1]
729 return height
730
731 return 0
732
733 def make_image_on_progressing_cb(self, tree):
734 self.current_angle_pos += self.step_angle
735 if self.running_mode == self.MODE_CYCLE_RUNNING:
736 if (self.current_angle_pos >= 1):
737 self.current_angle_pos = self.step_angle
738 else:
739 if self.current_angle_pos > 1:
740 self.force_stop()
741 return False
742
743 if self.is_queue_draw_row_area:
744 for path in self.running_cell_areas:
745 rect = tree.get_cell_area(path, tree.get_column(0))
746 row_x, _, row_width, _ = tree.get_visible_rect()
747 tree.queue_draw_area(row_x, rect.y + self.tree_headers_height, row_width, rect.height)
748 else:
749 for rect in self.running_cell_areas:
750 tree.queue_draw_area(rect.x, rect.y + self.tree_headers_height, rect.width, rect.height)
751
752 return (not self.force_stop_enable)
753
754 def append_running_cell_area(self, cell_area):
755 if cell_area and (cell_area not in self.running_cell_areas):
756 self.running_cell_areas.append(cell_area)
757
758 def remove_running_cell_area(self, cell_area):
759 if cell_area in self.running_cell_areas:
760 self.running_cell_areas.remove(cell_area)
761 if not self.running_cell_areas:
762 self.reset_run()
763
764gobject.type_register(HobCellRendererController)
765
766class HobCellRendererPixbuf(gtk.CellRendererPixbuf):
767 def __init__(self):
768 gtk.CellRendererPixbuf.__init__(self)
769 self.control = HobCellRendererController()
770 # add icon checker for make the gtk-icon transfer to hob-icon
771 self.checker = HobIconChecker()
772 self.set_property("stock-size", gtk.ICON_SIZE_DND)
773
774 def get_pixbuf_from_stock_icon(self, widget, stock_id="", size=gtk.ICON_SIZE_DIALOG):
775 if widget and stock_id and gtk.icon_factory_lookup_default(stock_id):
776 return widget.render_icon(stock_id, size)
777
778 return None
779
780 def set_icon_name_to_id(self, new_name):
781 if new_name and type(new_name) == str:
782 # check the name is need to transfer to hob icon or not
783 name = self.checker.check_stock_icon(new_name)
784 if name.startswith("hic") or name.startswith("gtk"):
785 stock_id = name
786 else:
787 stock_id = 'gtk-' + name
788
789 return stock_id
790
791 ''' render cell exactly, "icon-name" is priority
792 if use the 'hic-task-refresh' will make the pix animation
793 if 'pix' will change the pixbuf for it from the pixbuf or image.
794 '''
795 def do_render(self, window, tree, background_area,cell_area, expose_area, flags):
796 if (not self.control) or (not tree):
797 return
798
799 x, y, w, h = self.on_get_size(tree, cell_area)
800 x += cell_area.x
801 y += cell_area.y
802 w -= 2 * self.get_property("xpad")
803 h -= 2 * self.get_property("ypad")
804
805 stock_id = ""
806 if self.props.icon_name:
807 stock_id = self.set_icon_name_to_id(self.props.icon_name)
808 elif self.props.stock_id:
809 stock_id = self.props.stock_id
810 elif self.props.pixbuf:
811 pix = self.props.pixbuf
812 else:
813 return
814
815 if stock_id:
816 pix = self.get_pixbuf_from_stock_icon(tree, stock_id, self.props.stock_size)
817 if stock_id == 'hic-task-refresh':
818 self.control.append_running_cell_area(cell_area)
819 if self.control.is_active():
820 self.control.on_draw_pixbuf_cb(pix, window.cairo_create(), x, y, w, h, True)
821 else:
822 self.control.start_run(200, 0, 0, 1000, 150, tree)
823 else:
824 self.control.remove_running_cell_area(cell_area)
825 self.control.on_draw_pixbuf_cb(pix, window.cairo_create(), x, y, w, h, False)
826
827 def on_get_size(self, widget, cell_area):
828 if self.props.icon_name or self.props.pixbuf or self.props.stock_id:
829 w, h = gtk.icon_size_lookup(self.props.stock_size)
830 calc_width = self.get_property("xpad") * 2 + w
831 calc_height = self.get_property("ypad") * 2 + h
832 x_offset = 0
833 y_offset = 0
834 if cell_area and w > 0 and h > 0:
835 x_offset = self.get_property("xalign") * (cell_area.width - calc_width - self.get_property("xpad"))
836 y_offset = self.get_property("yalign") * (cell_area.height - calc_height - self.get_property("ypad"))
837
838 return x_offset, y_offset, w, h
839
840 return 0, 0, 0, 0
841
842gobject.type_register(HobCellRendererPixbuf)
843
844class HobCellRendererToggle(gtk.CellRendererToggle):
845 def __init__(self):
846 gtk.CellRendererToggle.__init__(self)
847 self.ctrl = HobCellRendererController(is_draw_row=True)
848 self.ctrl.running_mode = self.ctrl.MODE_ONE_SHORT
849 self.cell_attr = {"fadeout": False, "number_of_children": 0}
850
851 def do_render(self, window, widget, background_area, cell_area, expose_area, flags):
852 if (not self.ctrl) or (not widget):
853 return
854
855 if flags & gtk.CELL_RENDERER_SELECTED:
856 state = gtk.STATE_SELECTED
857 else:
858 state = gtk.STATE_NORMAL
859
860 if self.ctrl.is_active():
861 path = widget.get_path_at_pos(cell_area.x + cell_area.width/2, cell_area.y + cell_area.height/2)
862 # sometimes the parameters of cell_area will be a negative number,such as pull up down the scroll bar
863 # it's over the tree container range, so the path will be bad
864 if not path: return
865 path = path[0]
866 if path in self.ctrl.running_cell_areas:
867 cr = window.cairo_create()
868 color = widget.get_style().base[state]
869
870 row_x, _, row_width, _ = widget.get_visible_rect()
871 border_y = self.get_property("ypad")
872 self.ctrl.on_draw_fadeinout_cb(cr, color, row_x, cell_area.y - border_y, row_width, \
873 cell_area.height + border_y * 2, self.cell_attr["fadeout"])
874 # draw number of a group
875 if self.cell_attr["number_of_children"]:
876 text = "%d pkg" % self.cell_attr["number_of_children"]
877 pangolayout = widget.create_pango_layout(text)
878 textw, texth = pangolayout.get_pixel_size()
879 x = cell_area.x + (cell_area.width/2) - (textw/2)
880 y = cell_area.y + (cell_area.height/2) - (texth/2)
881
882 widget.style.paint_layout(window, state, True, cell_area, widget, "checkbox", x, y, pangolayout)
883 else:
884 return gtk.CellRendererToggle.do_render(self, window, widget, background_area, cell_area, expose_area, flags)
885
886 '''delay: normally delay time is 1000ms
887 cell_list: whilch cells need to be render
888 '''
889 def fadeout(self, tree, delay, cell_list=None):
890 if (delay < 200) or (not tree):
891 return
892 self.cell_attr["fadeout"] = True
893 self.ctrl.running_cell_areas = cell_list
894 self.ctrl.start_run(200, 0, 0, delay, (delay * 200 / 1000), tree)
895
896 def connect_render_state_changed(self, func, usrdata=None):
897 if not func:
898 return
899 if usrdata:
900 self.ctrl.connect("run-timer-stopped", func, self, usrdata)
901 else:
902 self.ctrl.connect("run-timer-stopped", func, self)
903
904gobject.type_register(HobCellRendererToggle)
diff --git a/bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py b/bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py
new file mode 100644
index 0000000000..79709d0d97
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py
@@ -0,0 +1,559 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2012 Intel Corporation
6#
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import glib
25import re
26from bb.ui.crumbs.progressbar import HobProgressBar
27from bb.ui.crumbs.hobcolor import HobColors
28from bb.ui.crumbs.hobwidget import hic, HobImageButton, HobInfoButton, HobAltButton, HobButton
29from bb.ui.crumbs.hoblistmodel import RecipeListModel
30from bb.ui.crumbs.hobpages import HobPage
31from bb.ui.crumbs.hig.retrieveimagedialog import RetrieveImageDialog
32
33#
34# ImageConfigurationPage
35#
36class ImageConfigurationPage (HobPage):
37
38 __dummy_machine__ = "--select a machine--"
39 __dummy_image__ = "--select an image recipe--"
40 __custom_image__ = "Select from my image recipes"
41
42 def __init__(self, builder):
43 super(ImageConfigurationPage, self).__init__(builder, "Image configuration")
44
45 self.image_combo_id = None
46 # we use machine_combo_changed_by_manual to identify the machine is changed by code
47 # or by manual. If by manual, all user's recipe selection and package selection are
48 # cleared.
49 self.machine_combo_changed_by_manual = True
50 self.stopping = False
51 self.warning_shift = 0
52 self.custom_image_selected = None
53 self.create_visual_elements()
54
55 def create_visual_elements(self):
56 # create visual elements
57 self.toolbar = gtk.Toolbar()
58 self.toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL)
59 self.toolbar.set_style(gtk.TOOLBAR_BOTH)
60
61 my_images_button = self.append_toolbar_button(self.toolbar,
62 "Images",
63 hic.ICON_IMAGES_DISPLAY_FILE,
64 hic.ICON_IMAGES_HOVER_FILE,
65 "Open previously built images",
66 self.my_images_button_clicked_cb)
67 settings_button = self.append_toolbar_button(self.toolbar,
68 "Settings",
69 hic.ICON_SETTINGS_DISPLAY_FILE,
70 hic.ICON_SETTINGS_HOVER_FILE,
71 "View additional build settings",
72 self.settings_button_clicked_cb)
73
74 self.config_top_button = self.add_onto_top_bar(self.toolbar)
75
76 self.gtable = gtk.Table(40, 40, True)
77 self.create_config_machine()
78 self.create_config_baseimg()
79 self.config_build_button = self.create_config_build_button()
80
81 def _remove_all_widget(self):
82 children = self.gtable.get_children() or []
83 for child in children:
84 self.gtable.remove(child)
85 children = self.box_group_area.get_children() or []
86 for child in children:
87 self.box_group_area.remove(child)
88 children = self.get_children() or []
89 for child in children:
90 self.remove(child)
91
92 def _pack_components(self, pack_config_build_button = False):
93 self._remove_all_widget()
94 self.pack_start(self.config_top_button, expand=False, fill=False)
95 self.pack_start(self.group_align, expand=True, fill=True)
96
97 self.box_group_area.pack_start(self.gtable, expand=True, fill=True)
98 if pack_config_build_button:
99 self.box_group_area.pack_end(self.config_build_button, expand=False, fill=False)
100 else:
101 box = gtk.HBox(False, 6)
102 box.show()
103 subbox = gtk.HBox(False, 0)
104 subbox.set_size_request(205, 49)
105 subbox.show()
106 box.add(subbox)
107 self.box_group_area.pack_end(box, False, False)
108
109 def show_machine(self):
110 self.progress_bar.reset()
111 self._pack_components(pack_config_build_button = False)
112 self.set_config_machine_layout(show_progress_bar = False)
113 self.show_all()
114
115 def update_progress_bar(self, title, fraction, status=None):
116 if self.stopping == False:
117 self.progress_bar.update(fraction)
118 self.progress_bar.set_text(title)
119 self.progress_bar.set_rcstyle(status)
120
121 def show_info_populating(self):
122 self._pack_components(pack_config_build_button = False)
123 self.set_config_machine_layout(show_progress_bar = True)
124 self.show_all()
125
126 def show_info_populated(self):
127 self.progress_bar.reset()
128 self._pack_components(pack_config_build_button = False)
129 self.set_config_machine_layout(show_progress_bar = False)
130 self.set_config_baseimg_layout()
131 self.show_all()
132
133 def show_baseimg_selected(self):
134 self.progress_bar.reset()
135 self._pack_components(pack_config_build_button = True)
136 self.set_config_machine_layout(show_progress_bar = False)
137 self.set_config_baseimg_layout()
138 self.show_all()
139 if self.builder.recipe_model.get_selected_image() == self.builder.recipe_model.__custom_image__:
140 self.just_bake_button.hide()
141
142 def add_warnings_bar(self):
143 #create the warnings bar shown when recipes parsing generates warnings
144 color = HobColors.KHAKI
145 warnings_bar = gtk.EventBox()
146 warnings_bar.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
147 warnings_bar.set_flags(gtk.CAN_DEFAULT)
148 warnings_bar.grab_default()
149
150 build_stop_tab = gtk.Table(10, 20, True)
151 warnings_bar.add(build_stop_tab)
152
153 icon = gtk.Image()
154 icon_pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_INDI_ALERT_FILE)
155 icon.set_from_pixbuf(icon_pix_buffer)
156 build_stop_tab.attach(icon, 0, 2, 0, 10)
157
158 label = gtk.Label()
159 label.set_alignment(0.0, 0.5)
160 warnings_nb = len(self.builder.parsing_warnings)
161 if warnings_nb == 1:
162 label.set_markup("<span size='x-large'><b>1 recipe parsing warning</b></span>")
163 else:
164 label.set_markup("<span size='x-large'><b>%s recipe parsing warnings</b></span>" % warnings_nb)
165 build_stop_tab.attach(label, 2, 12, 0, 10)
166
167 view_warnings_button = HobButton("View warnings")
168 view_warnings_button.connect('clicked', self.view_warnings_button_clicked_cb)
169 build_stop_tab.attach(view_warnings_button, 15, 19, 1, 9)
170
171 return warnings_bar
172
173 def disable_warnings_bar(self):
174 if self.builder.parsing_warnings:
175 if hasattr(self, 'warnings_bar'):
176 self.warnings_bar.hide_all()
177 self.builder.parsing_warnings = []
178
179 def create_config_machine(self):
180 self.machine_title = gtk.Label()
181 self.machine_title.set_alignment(0.0, 0.5)
182 mark = "<span %s>Select a machine</span>" % self.span_tag('x-large', 'bold')
183 self.machine_title.set_markup(mark)
184
185 self.machine_title_desc = gtk.Label()
186 self.machine_title_desc.set_alignment(0.0, 0.5)
187 mark = ("<span %s>Your selection is the profile of the target machine for which you"
188 " are building the image.\n</span>") % (self.span_tag('medium'))
189 self.machine_title_desc.set_markup(mark)
190
191 self.machine_combo = gtk.combo_box_new_text()
192 self.machine_combo.connect("changed", self.machine_combo_changed_cb)
193
194 icon_file = hic.ICON_LAYERS_DISPLAY_FILE
195 hover_file = hic.ICON_LAYERS_HOVER_FILE
196 self.layer_button = HobImageButton("Layers", "Add support for machines, software, etc.",
197 icon_file, hover_file)
198 self.layer_button.connect("clicked", self.layer_button_clicked_cb)
199
200 markup = "Layers are a powerful mechanism to extend the Yocto Project "
201 markup += "with your own functionality.\n"
202 markup += "For more on layers, check the <a href=\""
203 markup += "http://www.yoctoproject.org/docs/current/dev-manual/"
204 markup += "dev-manual.html#understanding-and-using-layers\">reference manual</a>."
205 self.layer_info_icon = HobInfoButton("<b>Layers</b>" + "*" + markup, self.get_parent())
206 self.progress_bar = HobProgressBar()
207 self.stop_button = HobAltButton("Stop")
208 self.stop_button.connect("clicked", self.stop_button_clicked_cb)
209 self.machine_separator = gtk.HSeparator()
210
211 def set_config_machine_layout(self, show_progress_bar = False):
212 self.gtable.attach(self.machine_title, 0, 40, 0, 4)
213 self.gtable.attach(self.machine_title_desc, 0, 40, 4, 6)
214 self.gtable.attach(self.machine_combo, 0, 12, 7, 10)
215 self.gtable.attach(self.layer_button, 14, 36, 7, 12)
216 self.gtable.attach(self.layer_info_icon, 36, 40, 7, 11)
217 if show_progress_bar:
218 #self.gtable.attach(self.progress_box, 0, 40, 15, 18)
219 self.gtable.attach(self.progress_bar, 0, 37, 15, 18)
220 self.gtable.attach(self.stop_button, 37, 40, 15, 18, 0, 0)
221 if self.builder.parsing_warnings:
222 self.warnings_bar = self.add_warnings_bar()
223 self.gtable.attach(self.warnings_bar, 0, 40, 14, 18)
224 self.warning_shift = 4
225 else:
226 self.warning_shift = 0
227 self.gtable.attach(self.machine_separator, 0, 40, 13, 14)
228
229 def create_config_baseimg(self):
230 self.image_title = gtk.Label()
231 self.image_title.set_alignment(0, 1.0)
232 mark = "<span %s>Select an image recipe</span>" % self.span_tag('x-large', 'bold')
233 self.image_title.set_markup(mark)
234
235 self.image_title_desc = gtk.Label()
236 self.image_title_desc.set_alignment(0, 0.5)
237
238 mark = ("<span %s>Image recipes are a starting point for the type of image you want. "
239 "You can build them as \n"
240 "they are or edit them to suit your needs.\n</span>") % self.span_tag('medium')
241 self.image_title_desc.set_markup(mark)
242
243 self.image_combo = gtk.combo_box_new_text()
244 self.image_combo.set_row_separator_func(self.combo_separator_func, None)
245 self.image_combo_id = self.image_combo.connect("changed", self.image_combo_changed_cb)
246
247 self.image_desc = gtk.Label()
248 self.image_desc.set_alignment(0.0, 0.5)
249 self.image_desc.set_size_request(256, -1)
250 self.image_desc.set_justify(gtk.JUSTIFY_LEFT)
251 self.image_desc.set_line_wrap(True)
252
253 # button to view recipes
254 icon_file = hic.ICON_RCIPE_DISPLAY_FILE
255 hover_file = hic.ICON_RCIPE_HOVER_FILE
256 self.view_adv_configuration_button = HobImageButton("Advanced configuration",
257 "Select image types, package formats, etc",
258 icon_file, hover_file)
259 self.view_adv_configuration_button.connect("clicked", self.view_adv_configuration_button_clicked_cb)
260
261 self.image_separator = gtk.HSeparator()
262
263 def combo_separator_func(self, model, iter, user_data):
264 name = model.get_value(iter, 0)
265 if name == "--Separator--":
266 return True
267
268 def set_config_baseimg_layout(self):
269 self.gtable.attach(self.image_title, 0, 40, 15+self.warning_shift, 17+self.warning_shift)
270 self.gtable.attach(self.image_title_desc, 0, 40, 18+self.warning_shift, 22+self.warning_shift)
271 self.gtable.attach(self.image_combo, 0, 12, 23+self.warning_shift, 26+self.warning_shift)
272 self.gtable.attach(self.image_desc, 0, 12, 27+self.warning_shift, 33+self.warning_shift)
273 self.gtable.attach(self.view_adv_configuration_button, 14, 36, 23+self.warning_shift, 28+self.warning_shift)
274 self.gtable.attach(self.image_separator, 0, 40, 35+self.warning_shift, 36+self.warning_shift)
275
276 def create_config_build_button(self):
277 # Create the "Build packages" and "Build image" buttons at the bottom
278 button_box = gtk.HBox(False, 6)
279
280 # create button "Build image"
281 self.just_bake_button = HobButton("Build image")
282 self.just_bake_button.set_tooltip_text("Build the image recipe as it is")
283 self.just_bake_button.connect("clicked", self.just_bake_button_clicked_cb)
284 button_box.pack_end(self.just_bake_button, expand=False, fill=False)
285
286 # create button "Edit image recipe"
287 self.edit_image_button = HobAltButton("Edit image recipe")
288 self.edit_image_button.set_tooltip_text("Customize the recipes and packages to be included in your image")
289 self.edit_image_button.connect("clicked", self.edit_image_button_clicked_cb)
290 button_box.pack_end(self.edit_image_button, expand=False, fill=False)
291
292 return button_box
293
294 def stop_button_clicked_cb(self, button):
295 self.stopping = True
296 self.progress_bar.set_text("Stopping recipe parsing")
297 self.progress_bar.set_rcstyle("stop")
298 self.builder.cancel_parse_sync()
299
300 def view_warnings_button_clicked_cb(self, button):
301 self.builder.show_warning_dialog()
302
303 def machine_combo_changed_idle_cb(self):
304 self.builder.window.set_cursor(None)
305
306 def machine_combo_changed_cb(self, machine_combo):
307 self.builder.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
308 self.builder.wait(0.1) #wait for combo and cursor to update
309 self.stopping = False
310 self.builder.parsing_warnings = []
311 combo_item = machine_combo.get_active_text()
312 if not combo_item or combo_item == self.__dummy_machine__:
313 return
314
315 # remove __dummy_machine__ item from the store list after first user selection
316 # because it is no longer valid
317 combo_store = machine_combo.get_model()
318 if len(combo_store) and (combo_store[0][0] == self.__dummy_machine__):
319 machine_combo.remove_text(0)
320
321 self.builder.configuration.curr_mach = combo_item
322 if self.machine_combo_changed_by_manual:
323 self.builder.configuration.clear_selection()
324 # reset machine_combo_changed_by_manual
325 self.machine_combo_changed_by_manual = True
326
327 self.builder.configuration.selected_image = None
328
329 # Do reparse recipes
330 self.builder.populate_recipe_package_info_async()
331
332 glib.idle_add(self.machine_combo_changed_idle_cb)
333
334 def update_machine_combo(self):
335 self.disable_warnings_bar()
336 all_machines = [self.__dummy_machine__] + self.builder.parameters.all_machines
337
338 model = self.machine_combo.get_model()
339 model.clear()
340 for machine in all_machines:
341 self.machine_combo.append_text(machine)
342 self.machine_combo.set_active(0)
343
344 def switch_machine_combo(self):
345 self.disable_warnings_bar()
346 self.machine_combo_changed_by_manual = False
347 model = self.machine_combo.get_model()
348 active = 0
349 while active < len(model):
350 if model[active][0] == self.builder.configuration.curr_mach:
351 self.machine_combo.set_active(active)
352 return
353 active += 1
354
355 if model[0][0] != self.__dummy_machine__:
356 self.machine_combo.insert_text(0, self.__dummy_machine__)
357
358 self.machine_combo.set_active(0)
359
360 def update_image_desc(self):
361 desc = ""
362 selected_image = self.image_combo.get_active_text()
363 if selected_image and selected_image in self.builder.recipe_model.pn_path.keys():
364 image_path = self.builder.recipe_model.pn_path[selected_image]
365 image_iter = self.builder.recipe_model.get_iter(image_path)
366 desc = self.builder.recipe_model.get_value(image_iter, self.builder.recipe_model.COL_DESC)
367
368 mark = ("<span %s>%s</span>\n") % (self.span_tag('small'), desc)
369 self.image_desc.set_markup(mark)
370
371 def image_combo_changed_idle_cb(self, selected_image, selected_recipes, selected_packages):
372 self.builder.update_recipe_model(selected_image, selected_recipes)
373 self.builder.update_package_model(selected_packages)
374 self.builder.window_sensitive(True)
375
376 def image_combo_changed_cb(self, combo):
377 self.builder.window_sensitive(False)
378 selected_image = self.image_combo.get_active_text()
379 if selected_image == self.__custom_image__:
380 topdir = self.builder.get_topdir()
381 images_dir = topdir + "/recipes/images/"
382 self.builder.ensure_dir(images_dir)
383
384 dialog = RetrieveImageDialog(images_dir, "Select from my image recipes",
385 self.builder, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
386 response = dialog.run()
387 if response == gtk.RESPONSE_OK:
388 image_name = dialog.get_filename()
389 head, tail = os.path.split(image_name)
390 selected_image = os.path.splitext(tail)[0]
391 self.custom_image_selected = selected_image
392 self.update_image_combo(self.builder.recipe_model, selected_image)
393 else:
394 selected_image = self.__dummy_image__
395 self.update_image_combo(self.builder.recipe_model, None)
396 dialog.destroy()
397 else:
398 if self.custom_image_selected:
399 self.custom_image_selected = None
400 self.update_image_combo(self.builder.recipe_model, selected_image)
401
402 if not selected_image or (selected_image == self.__dummy_image__):
403 self.builder.window_sensitive(True)
404 self.just_bake_button.hide()
405 self.edit_image_button.hide()
406 return
407
408 # remove __dummy_image__ item from the store list after first user selection
409 # because it is no longer valid
410 combo_store = combo.get_model()
411 if len(combo_store) and (combo_store[0][0] == self.__dummy_image__):
412 combo.remove_text(0)
413
414 self.builder.customized = False
415
416 selected_recipes = []
417
418 image_path = self.builder.recipe_model.pn_path[selected_image]
419 image_iter = self.builder.recipe_model.get_iter(image_path)
420 selected_packages = self.builder.recipe_model.get_value(image_iter, self.builder.recipe_model.COL_INSTALL).split()
421 self.update_image_desc()
422
423 self.builder.recipe_model.reset()
424 self.builder.package_model.reset()
425
426 self.show_baseimg_selected()
427
428 if selected_image == self.builder.recipe_model.__custom_image__:
429 self.just_bake_button.hide()
430
431 glib.idle_add(self.image_combo_changed_idle_cb, selected_image, selected_recipes, selected_packages)
432
433 def _image_combo_connect_signal(self):
434 if not self.image_combo_id:
435 self.image_combo_id = self.image_combo.connect("changed", self.image_combo_changed_cb)
436
437 def _image_combo_disconnect_signal(self):
438 if self.image_combo_id:
439 self.image_combo.disconnect(self.image_combo_id)
440 self.image_combo_id = None
441
442 def update_image_combo(self, recipe_model, selected_image):
443 # Update the image combo according to the images in the recipe_model
444 # populate image combo
445 filter = {RecipeListModel.COL_TYPE : ['image']}
446 image_model = recipe_model.tree_model(filter)
447 image_model.set_sort_column_id(recipe_model.COL_NAME, gtk.SORT_ASCENDING)
448 active = 0
449 cnt = 0
450
451 white_pattern = []
452 if self.builder.parameters.image_white_pattern:
453 for i in self.builder.parameters.image_white_pattern.split():
454 white_pattern.append(re.compile(i))
455
456 black_pattern = []
457 if self.builder.parameters.image_black_pattern:
458 for i in self.builder.parameters.image_black_pattern.split():
459 black_pattern.append(re.compile(i))
460 black_pattern.append(re.compile("hob-image"))
461
462 it = image_model.get_iter_first()
463 self._image_combo_disconnect_signal()
464 model = self.image_combo.get_model()
465 model.clear()
466 # Set a indicator text to combo store when first open
467 if not selected_image:
468 self.image_combo.append_text(self.__dummy_image__)
469 cnt = cnt + 1
470
471 self.image_combo.append_text(self.__custom_image__)
472 self.image_combo.append_text("--Separator--")
473 cnt = cnt + 2
474
475 topdir = self.builder.get_topdir()
476 # append and set active
477 while it:
478 path = image_model.get_path(it)
479 it = image_model.iter_next(it)
480 image_name = image_model[path][recipe_model.COL_NAME]
481 if image_name == self.builder.recipe_model.__custom_image__:
482 continue
483
484 if black_pattern:
485 allow = True
486 for pattern in black_pattern:
487 if pattern.search(image_name):
488 allow = False
489 break
490 elif white_pattern:
491 allow = False
492 for pattern in white_pattern:
493 if pattern.search(image_name):
494 allow = True
495 break
496 else:
497 allow = True
498
499 file_name = image_model[path][recipe_model.COL_FILE]
500 if file_name and topdir in file_name:
501 allow = False
502
503 if allow:
504 self.image_combo.append_text(image_name)
505 if image_name == selected_image:
506 active = cnt
507 cnt = cnt + 1
508 self.image_combo.append_text(self.builder.recipe_model.__custom_image__)
509
510 if selected_image == self.builder.recipe_model.__custom_image__:
511 active = cnt
512
513 if self.custom_image_selected:
514 self.image_combo.append_text("--Separator--")
515 self.image_combo.append_text(self.custom_image_selected)
516 cnt = cnt + 2
517 if self.custom_image_selected == selected_image:
518 active = cnt
519
520 self.image_combo.set_active(active)
521
522 if active != 0:
523 self.show_baseimg_selected()
524
525 self._image_combo_connect_signal()
526
527 def layer_button_clicked_cb(self, button):
528 # Create a layer selection dialog
529 self.builder.show_layer_selection_dialog()
530
531 def view_adv_configuration_button_clicked_cb(self, button):
532 # Create an advanced settings dialog
533 response, settings_changed = self.builder.show_adv_settings_dialog()
534 if not response:
535 return
536 if settings_changed:
537 self.builder.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
538 self.builder.wait(0.1) #wait for adv_settings_dialog to terminate
539 self.builder.reparse_post_adv_settings()
540 self.builder.window.set_cursor(None)
541
542 def just_bake_button_clicked_cb(self, button):
543 self.builder.parsing_warnings = []
544 self.builder.just_bake()
545
546 def edit_image_button_clicked_cb(self, button):
547 self.builder.configuration.initial_selected_image = self.builder.configuration.selected_image
548 self.builder.show_recipes()
549
550 def my_images_button_clicked_cb(self, button):
551 self.builder.show_load_my_images_dialog()
552
553 def settings_button_clicked_cb(self, button):
554 # Create an advanced settings dialog
555 response, settings_changed = self.builder.show_simple_settings_dialog()
556 if not response:
557 return
558 if settings_changed:
559 self.builder.reparse_post_adv_settings()
diff --git a/bitbake/lib/bb/ui/crumbs/imagedetailspage.py b/bitbake/lib/bb/ui/crumbs/imagedetailspage.py
new file mode 100755
index 0000000000..b5d9660218
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/imagedetailspage.py
@@ -0,0 +1,669 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2012 Intel Corporation
6#
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gobject
24import gtk
25from bb.ui.crumbs.hobcolor import HobColors
26from bb.ui.crumbs.hobwidget import hic, HobViewTable, HobAltButton, HobButton
27from bb.ui.crumbs.hobpages import HobPage
28import subprocess
29from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog
30from bb.ui.crumbs.hig.saveimagedialog import SaveImageDialog
31
32#
33# ImageDetailsPage
34#
35class ImageDetailsPage (HobPage):
36
37 class DetailBox (gtk.EventBox):
38 def __init__(self, widget = None, varlist = None, vallist = None, icon = None, button = None, button2=None, color = HobColors.LIGHT_GRAY):
39 gtk.EventBox.__init__(self)
40
41 # set color
42 style = self.get_style().copy()
43 style.bg[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(color, False, False)
44 self.set_style(style)
45
46 self.row = gtk.Table(1, 2, False)
47 self.row.set_border_width(10)
48 self.add(self.row)
49
50 total_rows = 0
51 if widget:
52 total_rows = 10
53 if varlist and vallist:
54 # pack the icon and the text on the left
55 total_rows += len(varlist)
56 self.table = gtk.Table(total_rows, 20, True)
57 self.table.set_row_spacings(6)
58 self.table.set_size_request(100, -1)
59 self.row.attach(self.table, 0, 1, 0, 1, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL)
60
61 colid = 0
62 rowid = 0
63 self.line_widgets = {}
64 if icon:
65 self.table.attach(icon, colid, colid + 2, 0, 1)
66 colid = colid + 2
67 if widget:
68 self.table.attach(widget, colid, 20, 0, 10)
69 rowid = 10
70 if varlist and vallist:
71 for row in range(rowid, total_rows):
72 index = row - rowid
73 self.line_widgets[varlist[index]] = self.text2label(varlist[index], vallist[index])
74 self.table.attach(self.line_widgets[varlist[index]], colid, 20, row, row + 1)
75 # pack the button on the right
76 if button:
77 self.bbox = gtk.VBox()
78 self.bbox.pack_start(button, expand=True, fill=False)
79 if button2:
80 self.bbox.pack_start(button2, expand=True, fill=False)
81 self.bbox.set_size_request(150,-1)
82 self.row.attach(self.bbox, 1, 2, 0, 1, xoptions=gtk.FILL, yoptions=gtk.EXPAND)
83
84 def update_line_widgets(self, variable, value):
85 if len(self.line_widgets) == 0:
86 return
87 if not isinstance(self.line_widgets[variable], gtk.Label):
88 return
89 self.line_widgets[variable].set_markup(self.format_line(variable, value))
90
91 def wrap_line(self, inputs):
92 # wrap the long text of inputs
93 wrap_width_chars = 75
94 outputs = ""
95 tmps = inputs
96 less_chars = len(inputs)
97 while (less_chars - wrap_width_chars) > 0:
98 less_chars -= wrap_width_chars
99 outputs += tmps[:wrap_width_chars] + "\n "
100 tmps = inputs[less_chars:]
101 outputs += tmps
102 return outputs
103
104 def format_line(self, variable, value):
105 wraped_value = self.wrap_line(value)
106 markup = "<span weight=\'bold\'>%s</span>" % variable
107 markup += "<span weight=\'normal\' foreground=\'#1c1c1c\' font_desc=\'14px\'>%s</span>" % wraped_value
108 return markup
109
110 def text2label(self, variable, value):
111 # append the name:value to the left box
112 # such as "Name: hob-core-minimal-variant-2011-12-15-beagleboard"
113 label = gtk.Label()
114 label.set_alignment(0.0, 0.5)
115 label.set_markup(self.format_line(variable, value))
116 return label
117
118 class BuildDetailBox (gtk.EventBox):
119 def __init__(self, varlist = None, vallist = None, icon = None, color = HobColors.LIGHT_GRAY):
120 gtk.EventBox.__init__(self)
121
122 # set color
123 style = self.get_style().copy()
124 style.bg[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(color, False, False)
125 self.set_style(style)
126
127 self.hbox = gtk.HBox()
128 self.hbox.set_border_width(10)
129 self.add(self.hbox)
130
131 total_rows = 0
132 if varlist and vallist:
133 # pack the icon and the text on the left
134 total_rows += len(varlist)
135 self.table = gtk.Table(total_rows, 20, True)
136 self.table.set_row_spacings(6)
137 self.table.set_size_request(100, -1)
138 self.hbox.pack_start(self.table, expand=True, fill=True, padding=15)
139
140 colid = 0
141 rowid = 0
142 self.line_widgets = {}
143 if icon:
144 self.table.attach(icon, colid, colid + 2, 0, 1)
145 colid = colid + 2
146 if varlist and vallist:
147 for row in range(rowid, total_rows):
148 index = row - rowid
149 self.line_widgets[varlist[index]] = self.text2label(varlist[index], vallist[index])
150 self.table.attach(self.line_widgets[varlist[index]], colid, 20, row, row + 1)
151
152 def update_line_widgets(self, variable, value):
153 if len(self.line_widgets) == 0:
154 return
155 if not isinstance(self.line_widgets[variable], gtk.Label):
156 return
157 self.line_widgets[variable].set_markup(self.format_line(variable, value))
158
159 def wrap_line(self, inputs):
160 # wrap the long text of inputs
161 wrap_width_chars = 75
162 outputs = ""
163 tmps = inputs
164 less_chars = len(inputs)
165 while (less_chars - wrap_width_chars) > 0:
166 less_chars -= wrap_width_chars
167 outputs += tmps[:wrap_width_chars] + "\n "
168 tmps = inputs[less_chars:]
169 outputs += tmps
170 return outputs
171
172 def format_line(self, variable, value):
173 wraped_value = self.wrap_line(value)
174 markup = "<span weight=\'bold\'>%s</span>" % variable
175 markup += "<span weight=\'normal\' foreground=\'#1c1c1c\' font_desc=\'14px\'>%s</span>" % wraped_value
176 return markup
177
178 def text2label(self, variable, value):
179 # append the name:value to the left box
180 # such as "Name: hob-core-minimal-variant-2011-12-15-beagleboard"
181 label = gtk.Label()
182 label.set_alignment(0.0, 0.5)
183 label.set_markup(self.format_line(variable, value))
184 return label
185
186 def __init__(self, builder):
187 super(ImageDetailsPage, self).__init__(builder, "Image details")
188
189 self.image_store = []
190 self.button_ids = {}
191 self.details_bottom_buttons = gtk.HBox(False, 6)
192 self.image_saved = False
193 self.create_visual_elements()
194 self.name_field_template = ""
195 self.description_field_template = ""
196
197 def create_visual_elements(self):
198 # create visual elements
199 # create the toolbar
200 self.toolbar = gtk.Toolbar()
201 self.toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL)
202 self.toolbar.set_style(gtk.TOOLBAR_BOTH)
203
204 my_images_button = self.append_toolbar_button(self.toolbar,
205 "Images",
206 hic.ICON_IMAGES_DISPLAY_FILE,
207 hic.ICON_IMAGES_HOVER_FILE,
208 "Open previously built images",
209 self.my_images_button_clicked_cb)
210 settings_button = self.append_toolbar_button(self.toolbar,
211 "Settings",
212 hic.ICON_SETTINGS_DISPLAY_FILE,
213 hic.ICON_SETTINGS_HOVER_FILE,
214 "View additional build settings",
215 self.settings_button_clicked_cb)
216
217 self.details_top_buttons = self.add_onto_top_bar(self.toolbar)
218
219 def _remove_all_widget(self):
220 children = self.get_children() or []
221 for child in children:
222 self.remove(child)
223 children = self.box_group_area.get_children() or []
224 for child in children:
225 self.box_group_area.remove(child)
226 children = self.details_bottom_buttons.get_children() or []
227 for child in children:
228 self.details_bottom_buttons.remove(child)
229
230 def show_page(self, step):
231 self.build_succeeded = (step == self.builder.IMAGE_GENERATED)
232 image_addr = self.builder.parameters.image_addr
233 image_names = self.builder.parameters.image_names
234 if self.build_succeeded:
235 machine = self.builder.configuration.curr_mach
236 base_image = self.builder.recipe_model.get_selected_image()
237 layers = self.builder.configuration.layers
238 pkg_num = "%s" % len(self.builder.package_model.get_selected_packages())
239 log_file = self.builder.current_logfile
240 else:
241 pkg_num = "N/A"
242 log_file = None
243
244 # remove
245 for button_id, button in self.button_ids.items():
246 button.disconnect(button_id)
247 self._remove_all_widget()
248
249 # repack
250 self.pack_start(self.details_top_buttons, expand=False, fill=False)
251 self.pack_start(self.group_align, expand=True, fill=True)
252
253 self.build_result = None
254 if self.image_saved or (self.build_succeeded and self.builder.current_step == self.builder.IMAGE_GENERATING):
255 # building is the previous step
256 icon = gtk.Image()
257 pixmap_path = hic.ICON_INDI_CONFIRM_FILE
258 color = HobColors.RUNNING
259 pix_buffer = gtk.gdk.pixbuf_new_from_file(pixmap_path)
260 icon.set_from_pixbuf(pix_buffer)
261 varlist = [""]
262 if self.image_saved:
263 vallist = ["Your image recipe has been saved"]
264 else:
265 vallist = ["Your image is ready"]
266 self.build_result = self.BuildDetailBox(varlist=varlist, vallist=vallist, icon=icon, color=color)
267 self.box_group_area.pack_start(self.build_result, expand=False, fill=False)
268
269 self.buttonlist = ["Build new image", "Save image recipe", "Run image", "Deploy image"]
270
271 # Name
272 self.image_store = []
273 self.toggled_image = ""
274 default_image_size = 0
275 self.num_toggled = 0
276 i = 0
277 for image_name in image_names:
278 image_size = HobPage._size_to_string(os.stat(os.path.join(image_addr, image_name)).st_size)
279
280 image_attr = ("run" if (self.test_type_runnable(image_name) and self.test_mach_runnable(image_name)) else \
281 ("deploy" if self.test_deployable(image_name) else ""))
282 is_toggled = (image_attr != "")
283
284 if not self.toggled_image:
285 if i == (len(image_names) - 1):
286 is_toggled = True
287 if is_toggled:
288 default_image_size = image_size
289 self.toggled_image = image_name
290
291 split_stuff = image_name.split('.')
292 if "rootfs" in split_stuff:
293 image_type = image_name[(len(split_stuff[0]) + len(".rootfs") + 1):]
294 else:
295 image_type = image_name[(len(split_stuff[0]) + 1):]
296
297 self.image_store.append({'name': image_name,
298 'type': image_type,
299 'size': image_size,
300 'is_toggled': is_toggled,
301 'action_attr': image_attr,})
302
303 i = i + 1
304 self.num_toggled += is_toggled
305
306 is_runnable = self.create_bottom_buttons(self.buttonlist, self.toggled_image)
307
308 # Generated image files info
309 varlist = ["Name: ", "Files created: ", "Directory: "]
310 vallist = []
311
312 vallist.append(image_name.split('.')[0])
313 vallist.append(', '.join(fileitem['type'] for fileitem in self.image_store))
314 vallist.append(image_addr)
315
316 view_files_button = HobAltButton("View files")
317 view_files_button.connect("clicked", self.view_files_clicked_cb, image_addr)
318 view_files_button.set_tooltip_text("Open the directory containing the image files")
319 open_log_button = None
320 if log_file:
321 open_log_button = HobAltButton("Open log")
322 open_log_button.connect("clicked", self.open_log_clicked_cb, log_file)
323 open_log_button.set_tooltip_text("Open the build's log file")
324 self.image_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=view_files_button, button2=open_log_button)
325 self.box_group_area.pack_start(self.image_detail, expand=False, fill=True)
326
327 # The default kernel box for the qemu images
328 self.sel_kernel = ""
329 self.kernel_detail = None
330 if 'qemu' in image_name:
331 self.sel_kernel = self.get_kernel_file_name()
332
333 # varlist = ["Kernel: "]
334 # vallist = []
335 # vallist.append(self.sel_kernel)
336
337 # change_kernel_button = HobAltButton("Change")
338 # change_kernel_button.connect("clicked", self.change_kernel_cb)
339 # change_kernel_button.set_tooltip_text("Change qemu kernel file")
340 # self.kernel_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=change_kernel_button)
341 # self.box_group_area.pack_start(self.kernel_detail, expand=True, fill=True)
342
343 # Machine, Image recipe and Layers
344 layer_num_limit = 15
345 varlist = ["Machine: ", "Image recipe: ", "Layers: "]
346 vallist = []
347 self.setting_detail = None
348 if self.build_succeeded:
349 vallist.append(machine)
350 if self.builder.recipe_model.is_custom_image():
351 if self.builder.configuration.initial_selected_image == self.builder.recipe_model.__custom_image__:
352 base_image ="New image recipe"
353 else:
354 base_image = self.builder.configuration.initial_selected_image + " (edited)"
355 vallist.append(base_image)
356 i = 0
357 for layer in layers:
358 varlist.append(" - ")
359 if i > layer_num_limit:
360 break
361 i += 1
362 vallist.append("")
363 i = 0
364 for layer in layers:
365 if i > layer_num_limit:
366 break
367 elif i == layer_num_limit:
368 vallist.append("and more...")
369 else:
370 vallist.append(layer)
371 i += 1
372
373 edit_config_button = HobAltButton("Edit configuration")
374 edit_config_button.set_tooltip_text("Edit machine and image recipe")
375 edit_config_button.connect("clicked", self.edit_config_button_clicked_cb)
376 self.setting_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=edit_config_button)
377 self.box_group_area.pack_start(self.setting_detail, expand=True, fill=True)
378
379 # Packages included, and Total image size
380 varlist = ["Packages included: ", "Total image size: "]
381 vallist = []
382 vallist.append(pkg_num)
383 vallist.append(default_image_size)
384 self.builder.configuration.image_size = default_image_size
385 self.builder.configuration.image_packages = self.builder.configuration.selected_packages
386 if self.build_succeeded:
387 edit_packages_button = HobAltButton("Edit packages")
388 edit_packages_button.set_tooltip_text("Edit the packages included in your image")
389 edit_packages_button.connect("clicked", self.edit_packages_button_clicked_cb)
390 else: # get to this page from "My images"
391 edit_packages_button = None
392 self.package_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=edit_packages_button)
393 self.box_group_area.pack_start(self.package_detail, expand=True, fill=True)
394
395 # pack the buttons at the bottom, at this time they are already created.
396 if self.build_succeeded:
397 self.box_group_area.pack_end(self.details_bottom_buttons, expand=False, fill=False)
398 else: # for "My images" page
399 self.details_separator = gtk.HSeparator()
400 self.box_group_area.pack_start(self.details_separator, expand=False, fill=False)
401 self.box_group_area.pack_start(self.details_bottom_buttons, expand=False, fill=False)
402
403 self.show_all()
404 if self.kernel_detail and (not is_runnable):
405 self.kernel_detail.hide()
406 self.image_saved = False
407
408 def view_files_clicked_cb(self, button, image_addr):
409 subprocess.call("xdg-open /%s" % image_addr, shell=True)
410
411 def open_log_clicked_cb(self, button, log_file):
412 if log_file:
413 log_file = "file:///" + log_file
414 gtk.show_uri(screen=button.get_screen(), uri=log_file, timestamp=0)
415
416 def refresh_package_detail_box(self, image_size):
417 self.package_detail.update_line_widgets("Total image size: ", image_size)
418
419 def test_type_runnable(self, image_name):
420 type_runnable = False
421 for t in self.builder.parameters.runnable_image_types:
422 if image_name.endswith(t):
423 type_runnable = True
424 break
425 return type_runnable
426
427 def test_mach_runnable(self, image_name):
428 mach_runnable = False
429 for t in self.builder.parameters.runnable_machine_patterns:
430 if t in image_name:
431 mach_runnable = True
432 break
433 return mach_runnable
434
435 def test_deployable(self, image_name):
436 if self.builder.configuration.curr_mach.startswith("qemu"):
437 return False
438 deployable = False
439 for t in self.builder.parameters.deployable_image_types:
440 if image_name.endswith(t):
441 deployable = True
442 break
443 return deployable
444
445 def get_kernel_file_name(self, kernel_addr=""):
446 kernel_name = ""
447
448 if not kernel_addr:
449 kernel_addr = self.builder.parameters.image_addr
450
451 files = [f for f in os.listdir(kernel_addr) if f[0] <> '.']
452 for check_file in files:
453 if check_file.endswith(".bin"):
454 name_splits = check_file.split(".")[0]
455 if self.builder.parameters.kernel_image_type in name_splits.split("-"):
456 kernel_name = check_file
457 break
458
459 return kernel_name
460
461 def show_builded_images_dialog(self, widget, primary_action=""):
462 title = primary_action if primary_action else "Your builded images"
463 dialog = CrumbsDialog(title, self.builder,
464 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
465 dialog.set_border_width(12)
466
467 label = gtk.Label()
468 label.set_use_markup(True)
469 label.set_alignment(0.0, 0.5)
470 label.set_padding(12,0)
471 if primary_action == "Run image":
472 label.set_markup("<span font_desc='12'>Select the image file you want to run:</span>")
473 elif primary_action == "Deploy image":
474 label.set_markup("<span font_desc='12'>Select the image file you want to deploy:</span>")
475 else:
476 label.set_markup("<span font_desc='12'>Select the image file you want to %s</span>" % primary_action)
477 dialog.vbox.pack_start(label, expand=False, fill=False)
478
479 # filter created images as action attribution (deploy or run)
480 action_attr = ""
481 action_images = []
482 for fileitem in self.image_store:
483 action_attr = fileitem['action_attr']
484 if (action_attr == 'run' and primary_action == "Run image") \
485 or (action_attr == 'deploy' and primary_action == "Deploy image"):
486 action_images.append(fileitem)
487
488 # pack the corresponding 'runnable' or 'deploy' radio_buttons, if there has no more than one file.
489 # assume that there does not both have 'deploy' and 'runnable' files in the same building result
490 # in possible as design.
491 curr_row = 0
492 rows = (len(action_images)) if len(action_images) < 10 else 10
493 table = gtk.Table(rows, 10, True)
494 table.set_row_spacings(6)
495 table.set_col_spacing(0, 12)
496 table.set_col_spacing(5, 12)
497
498 sel_parent_btn = None
499 for fileitem in action_images:
500 sel_btn = gtk.RadioButton(sel_parent_btn, fileitem['type'])
501 sel_parent_btn = sel_btn if not sel_parent_btn else sel_parent_btn
502 sel_btn.set_active(fileitem['is_toggled'])
503 sel_btn.connect('toggled', self.table_selected_cb, fileitem)
504 if curr_row < 10:
505 table.attach(sel_btn, 0, 4, curr_row, curr_row + 1, xpadding=24)
506 else:
507 table.attach(sel_btn, 5, 9, curr_row - 10, curr_row - 9, xpadding=24)
508 curr_row += 1
509
510 dialog.vbox.pack_start(table, expand=False, fill=False, padding=6)
511
512 button = dialog.add_button("Cancel", gtk.RESPONSE_CANCEL)
513 HobAltButton.style_button(button)
514
515 if primary_action:
516 button = dialog.add_button(primary_action, gtk.RESPONSE_YES)
517 HobButton.style_button(button)
518
519 dialog.show_all()
520
521 response = dialog.run()
522 dialog.destroy()
523
524 if response != gtk.RESPONSE_YES:
525 return
526
527 for fileitem in self.image_store:
528 if fileitem['is_toggled']:
529 if fileitem['action_attr'] == 'run':
530 self.builder.runqemu_image(fileitem['name'], self.sel_kernel)
531 elif fileitem['action_attr'] == 'deploy':
532 self.builder.deploy_image(fileitem['name'])
533
534 def table_selected_cb(self, tbutton, image):
535 image['is_toggled'] = tbutton.get_active()
536 if image['is_toggled']:
537 self.toggled_image = image['name']
538
539 def change_kernel_cb(self, widget):
540 kernel_path = self.builder.show_load_kernel_dialog()
541 if kernel_path and self.kernel_detail:
542 import os.path
543 self.sel_kernel = os.path.basename(kernel_path)
544 markup = self.kernel_detail.format_line("Kernel: ", self.sel_kernel)
545 label = ((self.kernel_detail.get_children()[0]).get_children()[0]).get_children()[0]
546 label.set_markup(markup)
547
548 def create_bottom_buttons(self, buttonlist, image_name):
549 # Create the buttons at the bottom
550 created = False
551 packed = False
552 self.button_ids = {}
553 is_runnable = False
554
555 # create button "Deploy image"
556 name = "Deploy image"
557 if name in buttonlist and self.test_deployable(image_name):
558 deploy_button = HobButton('Deploy image')
559 #deploy_button.set_size_request(205, 49)
560 deploy_button.set_tooltip_text("Burn a live image to a USB drive or flash memory")
561 deploy_button.set_flags(gtk.CAN_DEFAULT)
562 button_id = deploy_button.connect("clicked", self.deploy_button_clicked_cb)
563 self.button_ids[button_id] = deploy_button
564 self.details_bottom_buttons.pack_end(deploy_button, expand=False, fill=False)
565 created = True
566 packed = True
567
568 name = "Run image"
569 if name in buttonlist and self.test_type_runnable(image_name) and self.test_mach_runnable(image_name):
570 if created == True:
571 # separator
572 #label = gtk.Label(" or ")
573 #self.details_bottom_buttons.pack_end(label, expand=False, fill=False)
574
575 # create button "Run image"
576 run_button = HobAltButton("Run image")
577 else:
578 # create button "Run image" as the primary button
579 run_button = HobButton("Run image")
580 #run_button.set_size_request(205, 49)
581 run_button.set_flags(gtk.CAN_DEFAULT)
582 packed = True
583 run_button.set_tooltip_text("Start up an image with qemu emulator")
584 button_id = run_button.connect("clicked", self.run_button_clicked_cb)
585 self.button_ids[button_id] = run_button
586 self.details_bottom_buttons.pack_end(run_button, expand=False, fill=False)
587 created = True
588 is_runnable = True
589
590 name = "Save image recipe"
591 if name in buttonlist and self.builder.recipe_model.is_custom_image():
592 save_button = HobAltButton("Save image recipe")
593 save_button.set_tooltip_text("Keep your changes saving them as an image recipe")
594 save_button.set_sensitive(not self.image_saved)
595 button_id = save_button.connect("clicked", self.save_button_clicked_cb)
596 self.button_ids[button_id] = save_button
597 self.details_bottom_buttons.pack_end(save_button, expand=False, fill=False)
598
599 name = "Build new image"
600 if name in buttonlist:
601 # create button "Build new image"
602 if packed:
603 build_new_button = HobAltButton("Build new image")
604 else:
605 build_new_button = HobButton("Build new image")
606 build_new_button.set_flags(gtk.CAN_DEFAULT)
607 #build_new_button.set_size_request(205, 49)
608 self.details_bottom_buttons.pack_end(build_new_button, expand=False, fill=False)
609 build_new_button.set_tooltip_text("Create a new image from scratch")
610 button_id = build_new_button.connect("clicked", self.build_new_button_clicked_cb)
611 self.button_ids[button_id] = build_new_button
612
613 return is_runnable
614
615 def deploy_button_clicked_cb(self, button):
616 if self.toggled_image:
617 if self.num_toggled > 1:
618 self.set_sensitive(False)
619 self.show_builded_images_dialog(None, "Deploy image")
620 self.set_sensitive(True)
621 else:
622 self.builder.deploy_image(self.toggled_image)
623
624 def run_button_clicked_cb(self, button):
625 if self.toggled_image:
626 if self.num_toggled > 1:
627 self.set_sensitive(False)
628 self.show_builded_images_dialog(None, "Run image")
629 self.set_sensitive(True)
630 else:
631 self.builder.runqemu_image(self.toggled_image, self.sel_kernel)
632
633 def save_button_clicked_cb(self, button):
634 topdir = self.builder.get_topdir()
635 images_dir = topdir + "/recipes/images/"
636 self.builder.ensure_dir(images_dir)
637
638 self.name_field_template = self.builder.image_configuration_page.custom_image_selected
639 if self.name_field_template:
640 image_path = self.builder.recipe_model.pn_path[self.name_field_template]
641 image_iter = self.builder.recipe_model.get_iter(image_path)
642 self.description_field_template = self.builder.recipe_model.get_value(image_iter, self.builder.recipe_model.COL_DESC)
643 else:
644 self.name_field_template = ""
645
646 dialog = SaveImageDialog(images_dir, self.name_field_template, self.description_field_template,
647 "Save image recipe", self.builder, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
648 response = dialog.run()
649 dialog.destroy()
650
651 def build_new_button_clicked_cb(self, button):
652 self.builder.initiate_new_build_async()
653
654 def edit_config_button_clicked_cb(self, button):
655 self.builder.show_configuration()
656
657 def edit_packages_button_clicked_cb(self, button):
658 self.builder.show_packages(ask=False)
659
660 def my_images_button_clicked_cb(self, button):
661 self.builder.show_load_my_images_dialog()
662
663 def settings_button_clicked_cb(self, button):
664 # Create an advanced settings dialog
665 response, settings_changed = self.builder.show_simple_settings_dialog()
666 if not response:
667 return
668 if settings_changed:
669 self.builder.reparse_post_adv_settings()
diff --git a/bitbake/lib/bb/ui/crumbs/packageselectionpage.py b/bitbake/lib/bb/ui/crumbs/packageselectionpage.py
new file mode 100755
index 0000000000..780f026470
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/packageselectionpage.py
@@ -0,0 +1,355 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2012 Intel Corporation
6#
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import glib
25from bb.ui.crumbs.hobcolor import HobColors
26from bb.ui.crumbs.hobwidget import HobViewTable, HobNotebook, HobAltButton, HobButton
27from bb.ui.crumbs.hoblistmodel import PackageListModel
28from bb.ui.crumbs.hobpages import HobPage
29
30#
31# PackageSelectionPage
32#
33class PackageSelectionPage (HobPage):
34
35 pages = [
36 {
37 'name' : 'Included packages',
38 'tooltip' : 'The packages currently included for your image',
39 'filter' : { PackageListModel.COL_INC : [True] },
40 'search' : 'Search packages by name',
41 'searchtip' : 'Enter a package name to find it',
42 'columns' : [{
43 'col_name' : 'Package name',
44 'col_id' : PackageListModel.COL_NAME,
45 'col_style': 'text',
46 'col_min' : 100,
47 'col_max' : 300,
48 'expand' : 'True'
49 }, {
50 'col_name' : 'Size',
51 'col_id' : PackageListModel.COL_SIZE,
52 'col_style': 'text',
53 'col_min' : 100,
54 'col_max' : 300,
55 'expand' : 'True'
56 }, {
57 'col_name' : 'Recipe',
58 'col_id' : PackageListModel.COL_RCP,
59 'col_style': 'text',
60 'col_min' : 100,
61 'col_max' : 250,
62 'expand' : 'True'
63 }, {
64 'col_name' : 'Brought in by (+others)',
65 'col_id' : PackageListModel.COL_BINB,
66 'col_style': 'binb',
67 'col_min' : 100,
68 'col_max' : 350,
69 'expand' : 'True'
70 }, {
71 'col_name' : 'Included',
72 'col_id' : PackageListModel.COL_INC,
73 'col_style': 'check toggle',
74 'col_min' : 100,
75 'col_max' : 100
76 }]
77 }, {
78 'name' : 'All packages',
79 'tooltip' : 'All packages that have been built',
80 'filter' : {},
81 'search' : 'Search packages by name',
82 'searchtip' : 'Enter a package name to find it',
83 'columns' : [{
84 'col_name' : 'Package name',
85 'col_id' : PackageListModel.COL_NAME,
86 'col_style': 'text',
87 'col_min' : 100,
88 'col_max' : 400,
89 'expand' : 'True'
90 }, {
91 'col_name' : 'Size',
92 'col_id' : PackageListModel.COL_SIZE,
93 'col_style': 'text',
94 'col_min' : 100,
95 'col_max' : 500,
96 'expand' : 'True'
97 }, {
98 'col_name' : 'Recipe',
99 'col_id' : PackageListModel.COL_RCP,
100 'col_style': 'text',
101 'col_min' : 100,
102 'col_max' : 250,
103 'expand' : 'True'
104 }, {
105 'col_name' : 'Included',
106 'col_id' : PackageListModel.COL_INC,
107 'col_style': 'check toggle',
108 'col_min' : 100,
109 'col_max' : 100
110 }]
111 }
112 ]
113
114 (INCLUDED,
115 ALL) = range(2)
116
117 def __init__(self, builder):
118 super(PackageSelectionPage, self).__init__(builder, "Edit packages")
119
120 # set invisible members
121 self.recipe_model = self.builder.recipe_model
122 self.package_model = self.builder.package_model
123
124 # create visual elements
125 self.create_visual_elements()
126
127 def included_clicked_cb(self, button):
128 self.ins.set_current_page(self.INCLUDED)
129
130 def create_visual_elements(self):
131 self.label = gtk.Label("Packages included: 0\nSelected packages size: 0 MB")
132 self.eventbox = self.add_onto_top_bar(self.label, 73)
133 self.pack_start(self.eventbox, expand=False, fill=False)
134 self.pack_start(self.group_align, expand=True, fill=True)
135
136 # set visible members
137 self.ins = HobNotebook()
138 self.tables = [] # we need to modify table when the dialog is shown
139
140 search_names = []
141 search_tips = []
142 # append the tab
143 for page in self.pages:
144 columns = page['columns']
145 name = page['name']
146 tab = HobViewTable(columns, name)
147 search_names.append(page['search'])
148 search_tips.append(page['searchtip'])
149 filter = page['filter']
150 sort_model = self.package_model.tree_model(filter, initial=True)
151 tab.set_model(sort_model)
152 tab.connect("toggled", self.table_toggled_cb, name)
153 tab.connect("button-release-event", self.button_click_cb)
154 tab.connect("cell-fadeinout-stopped", self.after_fadeout_checkin_include, filter)
155 self.ins.append_page(tab, page['name'], page['tooltip'])
156 self.tables.append(tab)
157
158 self.ins.set_entry(search_names, search_tips)
159 self.ins.search.connect("changed", self.search_entry_changed)
160
161 # add all into the dialog
162 self.box_group_area.pack_start(self.ins, expand=True, fill=True)
163
164 self.button_box = gtk.HBox(False, 6)
165 self.box_group_area.pack_start(self.button_box, expand=False, fill=False)
166
167 self.build_image_button = HobButton('Build image')
168 #self.build_image_button.set_size_request(205, 49)
169 self.build_image_button.set_tooltip_text("Build target image")
170 self.build_image_button.set_flags(gtk.CAN_DEFAULT)
171 self.build_image_button.grab_default()
172 self.build_image_button.connect("clicked", self.build_image_clicked_cb)
173 self.button_box.pack_end(self.build_image_button, expand=False, fill=False)
174
175 self.back_button = HobAltButton('Cancel')
176 self.back_button.connect("clicked", self.back_button_clicked_cb)
177 self.button_box.pack_end(self.back_button, expand=False, fill=False)
178
179 def search_entry_changed(self, entry):
180 text = entry.get_text()
181 if self.ins.search_focus:
182 self.ins.search_focus = False
183 elif self.ins.page_changed:
184 self.ins.page_change = False
185 self.filter_search(entry)
186 elif text not in self.ins.search_names:
187 self.filter_search(entry)
188
189 def filter_search(self, entry):
190 text = entry.get_text()
191 current_tab = self.ins.get_current_page()
192 filter = self.pages[current_tab]['filter']
193 filter[PackageListModel.COL_NAME] = text
194 self.tables[current_tab].set_model(self.package_model.tree_model(filter, search_data=text))
195 if self.package_model.filtered_nb == 0:
196 if not self.ins.get_nth_page(current_tab).top_bar:
197 self.ins.get_nth_page(current_tab).add_no_result_bar(entry)
198 self.ins.get_nth_page(current_tab).top_bar.set_no_show_all(True)
199 self.ins.get_nth_page(current_tab).top_bar.show()
200 self.ins.get_nth_page(current_tab).scroll.hide()
201 else:
202 if self.ins.get_nth_page(current_tab).top_bar:
203 self.ins.get_nth_page(current_tab).top_bar.hide()
204 self.ins.get_nth_page(current_tab).scroll.show()
205 if entry.get_text() == '':
206 entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False)
207 else:
208 entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, True)
209
210 def button_click_cb(self, widget, event):
211 path, col = widget.table_tree.get_cursor()
212 tree_model = widget.table_tree.get_model()
213 if path and col.get_title() != 'Included': # else activation is likely a removal
214 properties = {'binb': '' , 'name': '', 'size':'', 'recipe':'', 'files_list':''}
215 properties['binb'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_BINB)
216 properties['name'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_NAME)
217 properties['size'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_SIZE)
218 properties['recipe'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_RCP)
219 properties['files_list'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_FLIST)
220
221 self.builder.show_recipe_property_dialog(properties)
222
223 def open_log_clicked_cb(self, button, log_file):
224 if log_file:
225 log_file = "file:///" + log_file
226 gtk.show_uri(screen=button.get_screen(), uri=log_file, timestamp=0)
227
228 def show_page(self, log_file):
229 children = self.button_box.get_children() or []
230 for child in children:
231 self.button_box.remove(child)
232 # re-packed the buttons as request, add the 'open log' button if build success
233 self.button_box.pack_end(self.build_image_button, expand=False, fill=False)
234 if log_file:
235 open_log_button = HobAltButton("Open log")
236 open_log_button.connect("clicked", self.open_log_clicked_cb, log_file)
237 open_log_button.set_tooltip_text("Open the build's log file")
238 self.button_box.pack_end(open_log_button, expand=False, fill=False)
239 self.button_box.pack_end(self.back_button, expand=False, fill=False)
240 self.show_all()
241
242 def build_image_clicked_cb(self, button):
243 self.builder.parsing_warnings = []
244 self.builder.build_image()
245
246 def refresh_tables(self):
247 self.ins.reset_entry(self.ins.search, 0)
248 for tab in self.tables:
249 index = self.tables.index(tab)
250 filter = self.pages[index]['filter']
251 tab.set_model(self.package_model.tree_model(filter, initial=True))
252
253 def back_button_clicked_cb(self, button):
254 if self.builder.previous_step == self.builder.IMAGE_GENERATED:
255 self.builder.restore_initial_selected_packages()
256 self.refresh_selection()
257 self.builder.show_image_details()
258 else:
259 self.builder.show_configuration()
260 self.refresh_tables()
261
262 def refresh_selection(self):
263 self.builder.configuration.selected_packages = self.package_model.get_selected_packages()
264 self.builder.configuration.user_selected_packages = self.package_model.get_user_selected_packages()
265 selected_packages_num = len(self.builder.configuration.selected_packages)
266 selected_packages_size = self.package_model.get_packages_size()
267 selected_packages_size_str = HobPage._size_to_string(selected_packages_size)
268
269 if self.builder.configuration.image_packages == self.builder.configuration.selected_packages:
270 image_total_size_str = self.builder.configuration.image_size
271 else:
272 image_overhead_factor = self.builder.configuration.image_overhead_factor
273 image_rootfs_size = self.builder.configuration.image_rootfs_size / 1024 # image_rootfs_size is KB
274 image_extra_size = self.builder.configuration.image_extra_size / 1024 # image_extra_size is KB
275 base_size = image_overhead_factor * selected_packages_size
276 image_total_size = max(base_size, image_rootfs_size) + image_extra_size
277 if "zypper" in self.builder.configuration.selected_packages:
278 image_total_size += (51200 * 1024)
279 image_total_size_str = HobPage._size_to_string(image_total_size)
280
281 self.label.set_label("Packages included: %s\nSelected packages size: %s\nEstimated image size: %s" %
282 (selected_packages_num, selected_packages_size_str, image_total_size_str))
283 self.ins.show_indicator_icon("Included packages", selected_packages_num)
284
285 def toggle_item_idle_cb(self, path, view_tree, cell, pagename):
286 if not self.package_model.path_included(path):
287 self.package_model.include_item(item_path=path, binb="User Selected")
288 else:
289 self.pre_fadeout_checkout_include(view_tree)
290 self.package_model.exclude_item(item_path=path)
291 self.render_fadeout(view_tree, cell)
292
293 self.refresh_selection()
294 if not self.builder.customized:
295 self.builder.customized = True
296 self.builder.configuration.initial_selected_image = self.builder.configuration.selected_image
297 self.builder.configuration.selected_image = self.recipe_model.__custom_image__
298 self.builder.rcppkglist_populated()
299
300 self.builder.window_sensitive(True)
301 view_model = view_tree.get_model()
302 vpath = self.package_model.convert_path_to_vpath(view_model, path)
303 view_tree.set_cursor(vpath)
304
305 def table_toggled_cb(self, table, cell, view_path, toggled_columnid, view_tree, pagename):
306 # Click to include a package
307 self.builder.window_sensitive(False)
308 view_model = view_tree.get_model()
309 path = self.package_model.convert_vpath_to_path(view_model, view_path)
310 glib.idle_add(self.toggle_item_idle_cb, path, view_tree, cell, pagename)
311
312 def pre_fadeout_checkout_include(self, tree):
313 #after the fadeout the table will be sorted as before
314 self.sort_column_id = self.package_model.sort_column_id
315 self.sort_order = self.package_model.sort_order
316
317 self.package_model.resync_fadeout_column(self.package_model.get_iter_first())
318 # Check out a model which base on the column COL_FADE_INC,
319 # it's save the prev state of column COL_INC before do exclude_item
320 filter = { PackageListModel.COL_FADE_INC : [True]}
321 new_model = self.package_model.tree_model(filter, excluded_items_ahead=True)
322 tree.set_model(new_model)
323 tree.expand_all()
324
325 def get_excluded_rows(self, to_render_cells, model, it):
326 while it:
327 path = model.get_path(it)
328 prev_cell_is_active = model.get_value(it, PackageListModel.COL_FADE_INC)
329 curr_cell_is_active = model.get_value(it, PackageListModel.COL_INC)
330 if (prev_cell_is_active == True) and (curr_cell_is_active == False):
331 to_render_cells.append(path)
332 if model.iter_has_child(it):
333 self.get_excluded_rows(to_render_cells, model, model.iter_children(it))
334 it = model.iter_next(it)
335
336 return to_render_cells
337
338 def render_fadeout(self, tree, cell):
339 if (not cell) or (not tree):
340 return
341 to_render_cells = []
342 view_model = tree.get_model()
343 self.get_excluded_rows(to_render_cells, view_model, view_model.get_iter_first())
344
345 cell.fadeout(tree, 1000, to_render_cells)
346
347 def after_fadeout_checkin_include(self, table, ctrl, cell, tree, filter):
348 self.package_model.sort_column_id = self.sort_column_id
349 self.package_model.sort_order = self.sort_order
350 tree.set_model(self.package_model.tree_model(filter))
351 tree.expand_all()
352
353 def set_packages_curr_tab(self, curr_page):
354 self.ins.set_current_page(curr_page)
355
diff --git a/bitbake/lib/bb/ui/crumbs/persistenttooltip.py b/bitbake/lib/bb/ui/crumbs/persistenttooltip.py
new file mode 100644
index 0000000000..927c194292
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/persistenttooltip.py
@@ -0,0 +1,186 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2012 Intel Corporation
5#
6# Authored by Joshua Lock <josh@linux.intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import gobject
22import gtk
23try:
24 import gconf
25except:
26 pass
27
28class PersistentTooltip(gtk.Window):
29 """
30 A tooltip which persists once shown until the user dismisses it with the Esc
31 key or by clicking the close button.
32
33 # FIXME: the PersistentTooltip should be disabled when the user clicks anywhere off
34 # it. We can't do this with focus-out-event becuase modal ensures we have focus?
35
36 markup: some Pango text markup to display in the tooltip
37 """
38 def __init__(self, markup, parent_win=None):
39 gtk.Window.__init__(self, gtk.WINDOW_POPUP)
40
41 # Inherit the system theme for a tooltip
42 style = gtk.rc_get_style_by_paths(gtk.settings_get_default(),
43 'gtk-tooltip', 'gtk-tooltip', gobject.TYPE_NONE)
44 self.set_style(style)
45
46 # The placement of the close button on the tip should reflect how the
47 # window manager of the users system places close buttons. Try to read
48 # the metacity gconf key to determine whether the close button is on the
49 # left or the right.
50 # In the case that we can't determine the users configuration we default
51 # to close buttons being on the right.
52 __button_right = True
53 try:
54 client = gconf.client_get_default()
55 order = client.get_string("/apps/metacity/general/button_layout")
56 if order and order.endswith(":"):
57 __button_right = False
58 except NameError:
59 pass
60
61 # We need to ensure we're only shown once
62 self.shown = False
63
64 # We don't want any WM decorations
65 self.set_decorated(False)
66 # We don't want to show in the taskbar or window switcher
67 self.set_skip_pager_hint(True)
68 self.set_skip_taskbar_hint(True)
69 # We must be modal to ensure we grab focus when presented from a gtk.Dialog
70 self.set_modal(True)
71
72 self.set_border_width(0)
73 self.set_position(gtk.WIN_POS_MOUSE)
74 self.set_opacity(0.95)
75
76 # Ensure a reasonable minimum size
77 self.set_geometry_hints(self, 100, 50)
78
79 # Set this window as a transient window for parent(main window)
80 if parent_win:
81 self.set_transient_for(parent_win)
82 self.set_destroy_with_parent(True)
83 # Draw our label and close buttons
84 hbox = gtk.HBox(False, 0)
85 hbox.show()
86 self.add(hbox)
87
88 img = gtk.Image()
89 img.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON)
90
91 self.button = gtk.Button()
92 self.button.set_image(img)
93 self.button.connect("clicked", self._dismiss_cb)
94 self.button.set_flags(gtk.CAN_DEFAULT)
95 self.button.grab_focus()
96 self.button.show()
97 vbox = gtk.VBox(False, 0)
98 vbox.show()
99 vbox.pack_start(self.button, False, False, 0)
100 if __button_right:
101 hbox.pack_end(vbox, True, True, 0)
102 else:
103 hbox.pack_start(vbox, True, True, 0)
104
105 self.set_default(self.button)
106
107 bin = gtk.HBox(True, 6)
108 bin.set_border_width(6)
109 bin.show()
110 self.label = gtk.Label()
111 self.label.set_line_wrap(True)
112 # We want to match the colours of the normal tooltips, as dictated by
113 # the users gtk+-2.0 theme, wherever possible - on some systems this
114 # requires explicitly setting a fg_color for the label which matches the
115 # tooltip_fg_color
116 settings = gtk.settings_get_default()
117 colours = settings.get_property('gtk-color-scheme').split('\n')
118 # remove any empty lines, there's likely to be a trailing one after
119 # calling split on a dictionary-like string
120 colours = filter(None, colours)
121 for col in colours:
122 item, val = col.split(': ')
123 if item == 'tooltip_fg_color':
124 style = self.label.get_style()
125 style.fg[gtk.STATE_NORMAL] = gtk.gdk.color_parse(val)
126 self.label.set_style(style)
127 break # we only care for the tooltip_fg_color
128
129 self.label.set_markup(markup)
130 self.label.show()
131 bin.add(self.label)
132 hbox.pack_end(bin, True, True, 6)
133
134 # add the original URL display for user reference
135 if 'a href' in markup:
136 hbox.set_tooltip_text(self.get_markup_url(markup))
137 hbox.show()
138
139 self.connect("key-press-event", self._catch_esc_cb)
140
141 """
142 Callback when the PersistentTooltip's close button is clicked.
143 Hides the PersistentTooltip.
144 """
145 def _dismiss_cb(self, button):
146 self.hide()
147 return True
148
149 """
150 Callback when the Esc key is detected. Hides the PersistentTooltip.
151 """
152 def _catch_esc_cb(self, widget, event):
153 keyname = gtk.gdk.keyval_name(event.keyval)
154 if keyname == "Escape":
155 self.hide()
156 return True
157
158 """
159 Called to present the PersistentTooltip.
160 Overrides the superclasses show() method to include state tracking.
161 """
162 def show(self):
163 if not self.shown:
164 self.shown = True
165 gtk.Window.show(self)
166
167 """
168 Called to hide the PersistentTooltip.
169 Overrides the superclasses hide() method to include state tracking.
170 """
171 def hide(self):
172 self.shown = False
173 gtk.Window.hide(self)
174
175 """
176 Called to get the hyperlink URL from markup text.
177 """
178 def get_markup_url(self, markup):
179 url = "http:"
180 if markup and type(markup) == str:
181 s = markup
182 if 'http:' in s:
183 import re
184 url = re.search('(http:[^,\\ "]+)', s).group(0)
185
186 return url
diff --git a/bitbake/lib/bb/ui/crumbs/progress.py b/bitbake/lib/bb/ui/crumbs/progress.py
new file mode 100644
index 0000000000..1d28a111b3
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/progress.py
@@ -0,0 +1,23 @@
1import gtk
2
3class ProgressBar(gtk.Dialog):
4 def __init__(self, parent):
5
6 gtk.Dialog.__init__(self, flags=(gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT))
7 self.set_title("Parsing metadata, please wait...")
8 self.set_default_size(500, 0)
9 self.set_transient_for(parent)
10 self.progress = gtk.ProgressBar()
11 self.vbox.pack_start(self.progress)
12 self.show_all()
13
14 def set_text(self, msg):
15 self.progress.set_text(msg)
16
17 def update(self, x, y):
18 self.progress.set_fraction(float(x)/float(y))
19 self.progress.set_text("%2d %%" % (x*100/y))
20
21 def pulse(self):
22 self.progress.set_text("Loading...")
23 self.progress.pulse()
diff --git a/bitbake/lib/bb/ui/crumbs/progressbar.py b/bitbake/lib/bb/ui/crumbs/progressbar.py
new file mode 100644
index 0000000000..3e2c660e4a
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/progressbar.py
@@ -0,0 +1,59 @@
1# BitBake Graphical GTK User Interface
2#
3# Copyright (C) 2011 Intel Corporation
4#
5# Authored by Shane Wang <shane.wang@intel.com>
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20import gtk
21from bb.ui.crumbs.hobcolor import HobColors
22
23class HobProgressBar (gtk.ProgressBar):
24 def __init__(self):
25 gtk.ProgressBar.__init__(self)
26 self.set_rcstyle(True)
27 self.percentage = 0
28
29 def set_rcstyle(self, status):
30 rcstyle = gtk.RcStyle()
31 rcstyle.fg[2] = gtk.gdk.Color(HobColors.BLACK)
32 if status == "stop":
33 rcstyle.bg[3] = gtk.gdk.Color(HobColors.WARNING)
34 elif status == "fail":
35 rcstyle.bg[3] = gtk.gdk.Color(HobColors.ERROR)
36 else:
37 rcstyle.bg[3] = gtk.gdk.Color(HobColors.RUNNING)
38 self.modify_style(rcstyle)
39
40 def set_title(self, text=None):
41 if not text:
42 text = ""
43 text += " %.0f%%" % self.percentage
44 self.set_text(text)
45
46 def set_stop_title(self, text=None):
47 if not text:
48 text = ""
49 self.set_text(text)
50
51 def reset(self):
52 self.set_fraction(0)
53 self.set_text("")
54 self.set_rcstyle(True)
55 self.percentage = 0
56
57 def update(self, fraction):
58 self.percentage = int(fraction * 100)
59 self.set_fraction(fraction)
diff --git a/bitbake/lib/bb/ui/crumbs/puccho.glade b/bitbake/lib/bb/ui/crumbs/puccho.glade
new file mode 100644
index 0000000000..d7553a6e14
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/puccho.glade
@@ -0,0 +1,606 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
3<!--Generated with glade3 3.4.5 on Mon Nov 10 12:24:12 2008 -->
4<glade-interface>
5 <widget class="GtkDialog" id="build_dialog">
6 <property name="title" translatable="yes">Start a build</property>
7 <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
8 <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
9 <property name="has_separator">False</property>
10 <child internal-child="vbox">
11 <widget class="GtkVBox" id="dialog-vbox1">
12 <property name="visible">True</property>
13 <property name="spacing">2</property>
14 <child>
15 <widget class="GtkTable" id="build_table">
16 <property name="visible">True</property>
17 <property name="border_width">6</property>
18 <property name="n_rows">7</property>
19 <property name="n_columns">3</property>
20 <property name="column_spacing">5</property>
21 <property name="row_spacing">6</property>
22 <child>
23 <widget class="GtkAlignment" id="status_alignment">
24 <property name="visible">True</property>
25 <property name="left_padding">12</property>
26 <child>
27 <widget class="GtkHBox" id="status_hbox">
28 <property name="spacing">6</property>
29 <child>
30 <widget class="GtkImage" id="status_image">
31 <property name="visible">True</property>
32 <property name="no_show_all">True</property>
33 <property name="xalign">0</property>
34 <property name="stock">gtk-dialog-error</property>
35 </widget>
36 <packing>
37 <property name="expand">False</property>
38 <property name="fill">False</property>
39 </packing>
40 </child>
41 <child>
42 <widget class="GtkLabel" id="status_label">
43 <property name="visible">True</property>
44 <property name="xalign">0</property>
45 <property name="label" translatable="yes">If you see this text something is wrong...</property>
46 <property name="use_markup">True</property>
47 <property name="use_underline">True</property>
48 </widget>
49 <packing>
50 <property name="position">1</property>
51 </packing>
52 </child>
53 </widget>
54 </child>
55 </widget>
56 <packing>
57 <property name="right_attach">3</property>
58 <property name="top_attach">2</property>
59 <property name="bottom_attach">3</property>
60 </packing>
61 </child>
62 <child>
63 <widget class="GtkLabel" id="label2">
64 <property name="visible">True</property>
65 <property name="xalign">0</property>
66 <property name="label" translatable="yes">&lt;b&gt;Build configuration&lt;/b&gt;</property>
67 <property name="use_markup">True</property>
68 </widget>
69 <packing>
70 <property name="right_attach">3</property>
71 <property name="top_attach">3</property>
72 <property name="bottom_attach">4</property>
73 <property name="y_options"></property>
74 </packing>
75 </child>
76 <child>
77 <widget class="GtkComboBox" id="image_combo">
78 <property name="visible">True</property>
79 <property name="sensitive">False</property>
80 </widget>
81 <packing>
82 <property name="left_attach">1</property>
83 <property name="right_attach">2</property>
84 <property name="top_attach">6</property>
85 <property name="bottom_attach">7</property>
86 <property name="y_options"></property>
87 </packing>
88 </child>
89 <child>
90 <widget class="GtkLabel" id="image_label">
91 <property name="visible">True</property>
92 <property name="sensitive">False</property>
93 <property name="xalign">0</property>
94 <property name="xpad">12</property>
95 <property name="label" translatable="yes">Image:</property>
96 </widget>
97 <packing>
98 <property name="top_attach">6</property>
99 <property name="bottom_attach">7</property>
100 <property name="y_options"></property>
101 </packing>
102 </child>
103 <child>
104 <widget class="GtkComboBox" id="distribution_combo">
105 <property name="visible">True</property>
106 <property name="sensitive">False</property>
107 </widget>
108 <packing>
109 <property name="left_attach">1</property>
110 <property name="right_attach">2</property>
111 <property name="top_attach">5</property>
112 <property name="bottom_attach">6</property>
113 <property name="y_options"></property>
114 </packing>
115 </child>
116 <child>
117 <widget class="GtkLabel" id="distribution_label">
118 <property name="visible">True</property>
119 <property name="sensitive">False</property>
120 <property name="xalign">0</property>
121 <property name="xpad">12</property>
122 <property name="label" translatable="yes">Distribution:</property>
123 </widget>
124 <packing>
125 <property name="top_attach">5</property>
126 <property name="bottom_attach">6</property>
127 <property name="y_options"></property>
128 </packing>
129 </child>
130 <child>
131 <widget class="GtkComboBox" id="machine_combo">
132 <property name="visible">True</property>
133 <property name="sensitive">False</property>
134 </widget>
135 <packing>
136 <property name="left_attach">1</property>
137 <property name="right_attach">2</property>
138 <property name="top_attach">4</property>
139 <property name="bottom_attach">5</property>
140 <property name="y_options"></property>
141 </packing>
142 </child>
143 <child>
144 <widget class="GtkLabel" id="machine_label">
145 <property name="visible">True</property>
146 <property name="sensitive">False</property>
147 <property name="xalign">0</property>
148 <property name="xpad">12</property>
149 <property name="label" translatable="yes">Machine:</property>
150 </widget>
151 <packing>
152 <property name="top_attach">4</property>
153 <property name="bottom_attach">5</property>
154 <property name="y_options"></property>
155 </packing>
156 </child>
157 <child>
158 <widget class="GtkButton" id="refresh_button">
159 <property name="visible">True</property>
160 <property name="sensitive">False</property>
161 <property name="can_focus">True</property>
162 <property name="receives_default">True</property>
163 <property name="label" translatable="yes">gtk-refresh</property>
164 <property name="use_stock">True</property>
165 <property name="response_id">0</property>
166 </widget>
167 <packing>
168 <property name="left_attach">2</property>
169 <property name="right_attach">3</property>
170 <property name="top_attach">1</property>
171 <property name="bottom_attach">2</property>
172 <property name="y_options"></property>
173 </packing>
174 </child>
175 <child>
176 <widget class="GtkEntry" id="location_entry">
177 <property name="visible">True</property>
178 <property name="can_focus">True</property>
179 <property name="width_chars">32</property>
180 </widget>
181 <packing>
182 <property name="left_attach">1</property>
183 <property name="right_attach">2</property>
184 <property name="top_attach">1</property>
185 <property name="bottom_attach">2</property>
186 <property name="y_options"></property>
187 </packing>
188 </child>
189 <child>
190 <widget class="GtkLabel" id="label3">
191 <property name="visible">True</property>
192 <property name="xalign">0</property>
193 <property name="xpad">12</property>
194 <property name="label" translatable="yes">Location:</property>
195 </widget>
196 <packing>
197 <property name="top_attach">1</property>
198 <property name="bottom_attach">2</property>
199 <property name="y_options"></property>
200 </packing>
201 </child>
202 <child>
203 <widget class="GtkLabel" id="label1">
204 <property name="visible">True</property>
205 <property name="xalign">0</property>
206 <property name="label" translatable="yes">&lt;b&gt;Repository&lt;/b&gt;</property>
207 <property name="use_markup">True</property>
208 </widget>
209 <packing>
210 <property name="right_attach">3</property>
211 <property name="y_options"></property>
212 </packing>
213 </child>
214 <child>
215 <widget class="GtkAlignment" id="alignment1">
216 <property name="visible">True</property>
217 <child>
218 <placeholder/>
219 </child>
220 </widget>
221 <packing>
222 <property name="left_attach">2</property>
223 <property name="right_attach">3</property>
224 <property name="top_attach">4</property>
225 <property name="bottom_attach">5</property>
226 <property name="y_options"></property>
227 </packing>
228 </child>
229 <child>
230 <widget class="GtkAlignment" id="alignment2">
231 <property name="visible">True</property>
232 <child>
233 <placeholder/>
234 </child>
235 </widget>
236 <packing>
237 <property name="left_attach">2</property>
238 <property name="right_attach">3</property>
239 <property name="top_attach">5</property>
240 <property name="bottom_attach">6</property>
241 <property name="y_options"></property>
242 </packing>
243 </child>
244 <child>
245 <widget class="GtkAlignment" id="alignment3">
246 <property name="visible">True</property>
247 <child>
248 <placeholder/>
249 </child>
250 </widget>
251 <packing>
252 <property name="left_attach">2</property>
253 <property name="right_attach">3</property>
254 <property name="top_attach">6</property>
255 <property name="bottom_attach">7</property>
256 <property name="y_options"></property>
257 </packing>
258 </child>
259 </widget>
260 <packing>
261 <property name="position">1</property>
262 </packing>
263 </child>
264 <child internal-child="action_area">
265 <widget class="GtkHButtonBox" id="dialog-action_area1">
266 <property name="visible">True</property>
267 <property name="layout_style">GTK_BUTTONBOX_END</property>
268 <child>
269 <placeholder/>
270 </child>
271 <child>
272 <placeholder/>
273 </child>
274 <child>
275 <placeholder/>
276 </child>
277 </widget>
278 <packing>
279 <property name="expand">False</property>
280 <property name="pack_type">GTK_PACK_END</property>
281 </packing>
282 </child>
283 </widget>
284 </child>
285 </widget>
286 <widget class="GtkDialog" id="dialog2">
287 <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
288 <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
289 <property name="has_separator">False</property>
290 <child internal-child="vbox">
291 <widget class="GtkVBox" id="dialog-vbox2">
292 <property name="visible">True</property>
293 <property name="spacing">2</property>
294 <child>
295 <widget class="GtkTable" id="table2">
296 <property name="visible">True</property>
297 <property name="border_width">6</property>
298 <property name="n_rows">7</property>
299 <property name="n_columns">3</property>
300 <property name="column_spacing">6</property>
301 <property name="row_spacing">6</property>
302 <child>
303 <widget class="GtkLabel" id="label7">
304 <property name="visible">True</property>
305 <property name="xalign">0</property>
306 <property name="label" translatable="yes">&lt;b&gt;Repositories&lt;/b&gt;</property>
307 <property name="use_markup">True</property>
308 </widget>
309 <packing>
310 <property name="right_attach">3</property>
311 <property name="y_options"></property>
312 </packing>
313 </child>
314 <child>
315 <widget class="GtkAlignment" id="alignment4">
316 <property name="visible">True</property>
317 <property name="xalign">0</property>
318 <property name="left_padding">12</property>
319 <child>
320 <widget class="GtkScrolledWindow" id="scrolledwindow1">
321 <property name="visible">True</property>
322 <property name="can_focus">True</property>
323 <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
324 <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
325 <child>
326 <widget class="GtkTreeView" id="treeview1">
327 <property name="visible">True</property>
328 <property name="can_focus">True</property>
329 <property name="headers_clickable">True</property>
330 </widget>
331 </child>
332 </widget>
333 </child>
334 </widget>
335 <packing>
336 <property name="right_attach">3</property>
337 <property name="top_attach">2</property>
338 <property name="bottom_attach">3</property>
339 <property name="y_options"></property>
340 </packing>
341 </child>
342 <child>
343 <widget class="GtkEntry" id="entry1">
344 <property name="visible">True</property>
345 <property name="can_focus">True</property>
346 </widget>
347 <packing>
348 <property name="left_attach">1</property>
349 <property name="right_attach">3</property>
350 <property name="top_attach">1</property>
351 <property name="bottom_attach">2</property>
352 <property name="y_options"></property>
353 </packing>
354 </child>
355 <child>
356 <widget class="GtkLabel" id="label9">
357 <property name="visible">True</property>
358 <property name="xalign">0</property>
359 <property name="label" translatable="yes">&lt;b&gt;Additional packages&lt;/b&gt;</property>
360 <property name="use_markup">True</property>
361 </widget>
362 <packing>
363 <property name="right_attach">3</property>
364 <property name="top_attach">4</property>
365 <property name="bottom_attach">5</property>
366 <property name="y_options"></property>
367 </packing>
368 </child>
369 <child>
370 <widget class="GtkAlignment" id="alignment6">
371 <property name="visible">True</property>
372 <property name="xalign">0</property>
373 <property name="xscale">0</property>
374 <child>
375 <widget class="GtkLabel" id="label8">
376 <property name="visible">True</property>
377 <property name="xalign">0</property>
378 <property name="yalign">0</property>
379 <property name="xpad">12</property>
380 <property name="label" translatable="yes">Location: </property>
381 </widget>
382 </child>
383 </widget>
384 <packing>
385 <property name="top_attach">1</property>
386 <property name="bottom_attach">2</property>
387 <property name="y_options"></property>
388 </packing>
389 </child>
390 <child>
391 <widget class="GtkAlignment" id="alignment7">
392 <property name="visible">True</property>
393 <property name="xalign">1</property>
394 <property name="xscale">0</property>
395 <child>
396 <widget class="GtkHButtonBox" id="hbuttonbox1">
397 <property name="visible">True</property>
398 <property name="spacing">5</property>
399 <child>
400 <widget class="GtkButton" id="button7">
401 <property name="visible">True</property>
402 <property name="can_focus">True</property>
403 <property name="receives_default">True</property>
404 <property name="label" translatable="yes">gtk-remove</property>
405 <property name="use_stock">True</property>
406 <property name="response_id">0</property>
407 </widget>
408 </child>
409 <child>
410 <widget class="GtkButton" id="button6">
411 <property name="visible">True</property>
412 <property name="can_focus">True</property>
413 <property name="receives_default">True</property>
414 <property name="label" translatable="yes">gtk-edit</property>
415 <property name="use_stock">True</property>
416 <property name="response_id">0</property>
417 </widget>
418 <packing>
419 <property name="position">1</property>
420 </packing>
421 </child>
422 <child>
423 <widget class="GtkButton" id="button5">
424 <property name="visible">True</property>
425 <property name="can_focus">True</property>
426 <property name="receives_default">True</property>
427 <property name="label" translatable="yes">gtk-add</property>
428 <property name="use_stock">True</property>
429 <property name="response_id">0</property>
430 </widget>
431 <packing>
432 <property name="position">2</property>
433 </packing>
434 </child>
435 </widget>
436 </child>
437 </widget>
438 <packing>
439 <property name="left_attach">1</property>
440 <property name="right_attach">3</property>
441 <property name="top_attach">3</property>
442 <property name="bottom_attach">4</property>
443 <property name="y_options"></property>
444 </packing>
445 </child>
446 <child>
447 <widget class="GtkAlignment" id="alignment5">
448 <property name="visible">True</property>
449 <child>
450 <placeholder/>
451 </child>
452 </widget>
453 <packing>
454 <property name="top_attach">3</property>
455 <property name="bottom_attach">4</property>
456 <property name="y_options"></property>
457 </packing>
458 </child>
459 <child>
460 <widget class="GtkLabel" id="label10">
461 <property name="visible">True</property>
462 <property name="xalign">0</property>
463 <property name="yalign">0</property>
464 <property name="xpad">12</property>
465 <property name="label" translatable="yes">Search:</property>
466 </widget>
467 <packing>
468 <property name="top_attach">5</property>
469 <property name="bottom_attach">6</property>
470 <property name="y_options"></property>
471 </packing>
472 </child>
473 <child>
474 <widget class="GtkEntry" id="entry2">
475 <property name="visible">True</property>
476 <property name="can_focus">True</property>
477 </widget>
478 <packing>
479 <property name="left_attach">1</property>
480 <property name="right_attach">3</property>
481 <property name="top_attach">5</property>
482 <property name="bottom_attach">6</property>
483 <property name="y_options"></property>
484 </packing>
485 </child>
486 <child>
487 <widget class="GtkAlignment" id="alignment8">
488 <property name="visible">True</property>
489 <property name="xalign">0</property>
490 <property name="left_padding">12</property>
491 <child>
492 <widget class="GtkScrolledWindow" id="scrolledwindow2">
493 <property name="visible">True</property>
494 <property name="can_focus">True</property>
495 <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
496 <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
497 <child>
498 <widget class="GtkTreeView" id="treeview2">
499 <property name="visible">True</property>
500 <property name="can_focus">True</property>
501 <property name="headers_clickable">True</property>
502 </widget>
503 </child>
504 </widget>
505 </child>
506 </widget>
507 <packing>
508 <property name="right_attach">3</property>
509 <property name="top_attach">6</property>
510 <property name="bottom_attach">7</property>
511 <property name="y_options"></property>
512 </packing>
513 </child>
514 </widget>
515 <packing>
516 <property name="position">1</property>
517 </packing>
518 </child>
519 <child internal-child="action_area">
520 <widget class="GtkHButtonBox" id="dialog-action_area2">
521 <property name="visible">True</property>
522 <property name="layout_style">GTK_BUTTONBOX_END</property>
523 <child>
524 <widget class="GtkButton" id="button4">
525 <property name="visible">True</property>
526 <property name="can_focus">True</property>
527 <property name="receives_default">True</property>
528 <property name="label" translatable="yes">gtk-close</property>
529 <property name="use_stock">True</property>
530 <property name="response_id">0</property>
531 </widget>
532 </child>
533 </widget>
534 <packing>
535 <property name="expand">False</property>
536 <property name="pack_type">GTK_PACK_END</property>
537 </packing>
538 </child>
539 </widget>
540 </child>
541 </widget>
542 <widget class="GtkWindow" id="main_window">
543 <child>
544 <widget class="GtkVBox" id="main_window_vbox">
545 <property name="visible">True</property>
546 <child>
547 <widget class="GtkToolbar" id="main_toolbar">
548 <property name="visible">True</property>
549 <child>
550 <widget class="GtkToolButton" id="main_toolbutton_build">
551 <property name="visible">True</property>
552 <property name="label" translatable="yes">Build</property>
553 <property name="stock_id">gtk-execute</property>
554 </widget>
555 <packing>
556 <property name="expand">False</property>
557 </packing>
558 </child>
559 </widget>
560 <packing>
561 <property name="expand">False</property>
562 </packing>
563 </child>
564 <child>
565 <widget class="GtkVPaned" id="vpaned1">
566 <property name="visible">True</property>
567 <property name="can_focus">True</property>
568 <child>
569 <widget class="GtkScrolledWindow" id="results_scrolledwindow">
570 <property name="visible">True</property>
571 <property name="can_focus">True</property>
572 <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
573 <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
574 <child>
575 <placeholder/>
576 </child>
577 </widget>
578 <packing>
579 <property name="resize">False</property>
580 <property name="shrink">True</property>
581 </packing>
582 </child>
583 <child>
584 <widget class="GtkScrolledWindow" id="progress_scrolledwindow">
585 <property name="visible">True</property>
586 <property name="can_focus">True</property>
587 <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
588 <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
589 <child>
590 <placeholder/>
591 </child>
592 </widget>
593 <packing>
594 <property name="resize">True</property>
595 <property name="shrink">True</property>
596 </packing>
597 </child>
598 </widget>
599 <packing>
600 <property name="position">1</property>
601 </packing>
602 </child>
603 </widget>
604 </child>
605 </widget>
606</glade-interface>
diff --git a/bitbake/lib/bb/ui/crumbs/recipeselectionpage.py b/bitbake/lib/bb/ui/crumbs/recipeselectionpage.py
new file mode 100755
index 0000000000..58db43f706
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/recipeselectionpage.py
@@ -0,0 +1,335 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2012 Intel Corporation
6#
7# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
8# Authored by Shane Wang <shane.wang@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import gtk
24import glib
25from bb.ui.crumbs.hobcolor import HobColors
26from bb.ui.crumbs.hobwidget import HobViewTable, HobNotebook, HobAltButton, HobButton
27from bb.ui.crumbs.hoblistmodel import RecipeListModel
28from bb.ui.crumbs.hobpages import HobPage
29
30#
31# RecipeSelectionPage
32#
33class RecipeSelectionPage (HobPage):
34 pages = [
35 {
36 'name' : 'Included recipes',
37 'tooltip' : 'The recipes currently included for your image',
38 'filter' : { RecipeListModel.COL_INC : [True],
39 RecipeListModel.COL_TYPE : ['recipe', 'packagegroup'] },
40 'search' : 'Search recipes by name',
41 'searchtip' : 'Enter a recipe name to find it',
42 'columns' : [{
43 'col_name' : 'Recipe name',
44 'col_id' : RecipeListModel.COL_NAME,
45 'col_style': 'text',
46 'col_min' : 100,
47 'col_max' : 400,
48 'expand' : 'True'
49 }, {
50 'col_name' : 'Group',
51 'col_id' : RecipeListModel.COL_GROUP,
52 'col_style': 'text',
53 'col_min' : 100,
54 'col_max' : 300,
55 'expand' : 'True'
56 }, {
57 'col_name' : 'Brought in by (+others)',
58 'col_id' : RecipeListModel.COL_BINB,
59 'col_style': 'binb',
60 'col_min' : 100,
61 'col_max' : 500,
62 'expand' : 'True'
63 }, {
64 'col_name' : 'Included',
65 'col_id' : RecipeListModel.COL_INC,
66 'col_style': 'check toggle',
67 'col_min' : 100,
68 'col_max' : 100
69 }]
70 }, {
71 'name' : 'All recipes',
72 'tooltip' : 'All recipes in your configured layers',
73 'filter' : { RecipeListModel.COL_TYPE : ['recipe'] },
74 'search' : 'Search recipes by name',
75 'searchtip' : 'Enter a recipe name to find it',
76 'columns' : [{
77 'col_name' : 'Recipe name',
78 'col_id' : RecipeListModel.COL_NAME,
79 'col_style': 'text',
80 'col_min' : 100,
81 'col_max' : 400,
82 'expand' : 'True'
83 }, {
84 'col_name' : 'Group',
85 'col_id' : RecipeListModel.COL_GROUP,
86 'col_style': 'text',
87 'col_min' : 100,
88 'col_max' : 400,
89 'expand' : 'True'
90 }, {
91 'col_name' : 'License',
92 'col_id' : RecipeListModel.COL_LIC,
93 'col_style': 'text',
94 'col_min' : 100,
95 'col_max' : 400,
96 'expand' : 'True'
97 }, {
98 'col_name' : 'Included',
99 'col_id' : RecipeListModel.COL_INC,
100 'col_style': 'check toggle',
101 'col_min' : 100,
102 'col_max' : 100
103 }]
104 }, {
105 'name' : 'Package Groups',
106 'tooltip' : 'All package groups in your configured layers',
107 'filter' : { RecipeListModel.COL_TYPE : ['packagegroup'] },
108 'search' : 'Search package groups by name',
109 'searchtip' : 'Enter a package group name to find it',
110 'columns' : [{
111 'col_name' : 'Package group name',
112 'col_id' : RecipeListModel.COL_NAME,
113 'col_style': 'text',
114 'col_min' : 100,
115 'col_max' : 400,
116 'expand' : 'True'
117 }, {
118 'col_name' : 'Included',
119 'col_id' : RecipeListModel.COL_INC,
120 'col_style': 'check toggle',
121 'col_min' : 100,
122 'col_max' : 100
123 }]
124 }
125 ]
126
127 (INCLUDED,
128 ALL,
129 TASKS) = range(3)
130
131 def __init__(self, builder = None):
132 super(RecipeSelectionPage, self).__init__(builder, "Step 1 of 2: Edit recipes")
133
134 # set invisible members
135 self.recipe_model = self.builder.recipe_model
136
137 # create visual elements
138 self.create_visual_elements()
139
140 def included_clicked_cb(self, button):
141 self.ins.set_current_page(self.INCLUDED)
142
143 def create_visual_elements(self):
144 self.eventbox = self.add_onto_top_bar(None, 73)
145 self.pack_start(self.eventbox, expand=False, fill=False)
146 self.pack_start(self.group_align, expand=True, fill=True)
147
148 # set visible members
149 self.ins = HobNotebook()
150 self.tables = [] # we need modify table when the dialog is shown
151
152 search_names = []
153 search_tips = []
154 # append the tabs in order
155 for page in self.pages:
156 columns = page['columns']
157 name = page['name']
158 tab = HobViewTable(columns, name)
159 search_names.append(page['search'])
160 search_tips.append(page['searchtip'])
161 filter = page['filter']
162 sort_model = self.recipe_model.tree_model(filter, initial=True)
163 tab.set_model(sort_model)
164 tab.connect("toggled", self.table_toggled_cb, name)
165 tab.connect("button-release-event", self.button_click_cb)
166 tab.connect("cell-fadeinout-stopped", self.after_fadeout_checkin_include, filter)
167 self.ins.append_page(tab, page['name'], page['tooltip'])
168 self.tables.append(tab)
169
170 self.ins.set_entry(search_names, search_tips)
171 self.ins.search.connect("changed", self.search_entry_changed)
172
173 # add all into the window
174 self.box_group_area.pack_start(self.ins, expand=True, fill=True)
175
176 button_box = gtk.HBox(False, 6)
177 self.box_group_area.pack_end(button_box, expand=False, fill=False)
178
179 self.build_packages_button = HobButton('Build packages')
180 #self.build_packages_button.set_size_request(205, 49)
181 self.build_packages_button.set_tooltip_text("Build selected recipes into packages")
182 self.build_packages_button.set_flags(gtk.CAN_DEFAULT)
183 self.build_packages_button.grab_default()
184 self.build_packages_button.connect("clicked", self.build_packages_clicked_cb)
185 button_box.pack_end(self.build_packages_button, expand=False, fill=False)
186
187 self.back_button = HobAltButton('Cancel')
188 self.back_button.connect("clicked", self.back_button_clicked_cb)
189 button_box.pack_end(self.back_button, expand=False, fill=False)
190
191 def search_entry_changed(self, entry):
192 text = entry.get_text()
193 if self.ins.search_focus:
194 self.ins.search_focus = False
195 elif self.ins.page_changed:
196 self.ins.page_change = False
197 self.filter_search(entry)
198 elif text not in self.ins.search_names:
199 self.filter_search(entry)
200
201 def filter_search(self, entry):
202 text = entry.get_text()
203 current_tab = self.ins.get_current_page()
204 filter = self.pages[current_tab]['filter']
205 filter[RecipeListModel.COL_NAME] = text
206 self.tables[current_tab].set_model(self.recipe_model.tree_model(filter, search_data=text))
207 if self.recipe_model.filtered_nb == 0:
208 if not self.ins.get_nth_page(current_tab).top_bar:
209 self.ins.get_nth_page(current_tab).add_no_result_bar(entry)
210 self.ins.get_nth_page(current_tab).top_bar.set_no_show_all(True)
211 self.ins.get_nth_page(current_tab).top_bar.show()
212 self.ins.get_nth_page(current_tab).scroll.hide()
213 else:
214 if self.ins.get_nth_page(current_tab).top_bar:
215 self.ins.get_nth_page(current_tab).top_bar.hide()
216 self.ins.get_nth_page(current_tab).scroll.show()
217 if entry.get_text() == '':
218 entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False)
219 else:
220 entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, True)
221
222 def button_click_cb(self, widget, event):
223 path, col = widget.table_tree.get_cursor()
224 tree_model = widget.table_tree.get_model()
225 if path and col.get_title() != 'Included': # else activation is likely a removal
226 properties = {'summary': '', 'name': '', 'version': '', 'revision': '', 'binb': '', 'group': '', 'license': '', 'homepage': '', 'bugtracker': '', 'description': ''}
227 properties['summary'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_SUMMARY)
228 properties['name'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_NAME)
229 properties['version'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_VERSION)
230 properties['revision'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_REVISION)
231 properties['binb'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_BINB)
232 properties['group'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_GROUP)
233 properties['license'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_LIC)
234 properties['homepage'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_HOMEPAGE)
235 properties['bugtracker'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_BUGTRACKER)
236 properties['description'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_DESC)
237 self.builder.show_recipe_property_dialog(properties)
238
239 def build_packages_clicked_cb(self, button):
240 self.refresh_tables()
241 self.builder.build_packages()
242
243 def refresh_tables(self):
244 self.ins.reset_entry(self.ins.search, 0)
245 for tab in self.tables:
246 index = self.tables.index(tab)
247 filter = self.pages[index]['filter']
248 tab.set_model(self.recipe_model.tree_model(filter, search_data="", initial=True))
249
250 def back_button_clicked_cb(self, button):
251 self.builder.recipe_model.set_selected_image(self.builder.configuration.initial_selected_image)
252 self.builder.image_configuration_page.update_image_combo(self.builder.recipe_model, self.builder.configuration.initial_selected_image)
253 self.builder.image_configuration_page.update_image_desc()
254 self.builder.show_configuration()
255 self.refresh_tables()
256
257 def refresh_selection(self):
258 self.builder.configuration.selected_image = self.recipe_model.get_selected_image()
259 _, self.builder.configuration.selected_recipes = self.recipe_model.get_selected_recipes()
260 self.ins.show_indicator_icon("Included recipes", len(self.builder.configuration.selected_recipes))
261
262 def toggle_item_idle_cb(self, path, view_tree, cell, pagename):
263 if not self.recipe_model.path_included(path):
264 self.recipe_model.include_item(item_path=path, binb="User Selected", image_contents=False)
265 else:
266 self.pre_fadeout_checkout_include(view_tree, pagename)
267 self.recipe_model.exclude_item(item_path=path)
268 self.render_fadeout(view_tree, cell)
269
270 self.refresh_selection()
271 if not self.builder.customized:
272 self.builder.customized = True
273 self.builder.configuration.selected_image = self.recipe_model.__custom_image__
274 self.builder.rcppkglist_populated()
275
276 self.builder.window_sensitive(True)
277
278 view_model = view_tree.get_model()
279 vpath = self.recipe_model.convert_path_to_vpath(view_model, path)
280 view_tree.set_cursor(vpath)
281
282 def table_toggled_cb(self, table, cell, view_path, toggled_columnid, view_tree, pagename):
283 # Click to include a recipe
284 self.builder.window_sensitive(False)
285 view_model = view_tree.get_model()
286 path = self.recipe_model.convert_vpath_to_path(view_model, view_path)
287 glib.idle_add(self.toggle_item_idle_cb, path, view_tree, cell, pagename)
288
289 def pre_fadeout_checkout_include(self, tree, pagename):
290 #after the fadeout the table will be sorted as before
291 self.sort_column_id = self.recipe_model.sort_column_id
292 self.sort_order = self.recipe_model.sort_order
293
294 #resync the included items to a backup fade include column
295 it = self.recipe_model.get_iter_first()
296 while it:
297 active = self.recipe_model.get_value(it, self.recipe_model.COL_INC)
298 self.recipe_model.set(it, self.recipe_model.COL_FADE_INC, active)
299 it = self.recipe_model.iter_next(it)
300 # Check out a model which base on the column COL_FADE_INC,
301 # it's save the prev state of column COL_INC before do exclude_item
302 filter = { RecipeListModel.COL_FADE_INC:[True] }
303 if pagename == "Included recipes":
304 filter[RecipeListModel.COL_TYPE] = ['recipe', 'packagegroup']
305 elif pagename == "All recipes":
306 filter[RecipeListModel.COL_TYPE] = ['recipe']
307 else:
308 filter[RecipeListModel.COL_TYPE] = ['packagegroup']
309
310 new_model = self.recipe_model.tree_model(filter, excluded_items_ahead=True)
311 tree.set_model(new_model)
312
313 def render_fadeout(self, tree, cell):
314 if (not cell) or (not tree):
315 return
316 to_render_cells = []
317 model = tree.get_model()
318 it = model.get_iter_first()
319 while it:
320 path = model.get_path(it)
321 prev_cell_is_active = model.get_value(it, RecipeListModel.COL_FADE_INC)
322 curr_cell_is_active = model.get_value(it, RecipeListModel.COL_INC)
323 if (prev_cell_is_active == True) and (curr_cell_is_active == False):
324 to_render_cells.append(path)
325 it = model.iter_next(it)
326
327 cell.fadeout(tree, 1000, to_render_cells)
328
329 def after_fadeout_checkin_include(self, table, ctrl, cell, tree, filter):
330 self.recipe_model.sort_column_id = self.sort_column_id
331 self.recipe_model.sort_order = self.sort_order
332 tree.set_model(self.recipe_model.tree_model(filter))
333
334 def set_recipe_curr_tab(self, curr_page):
335 self.ins.set_current_page(curr_page)
diff --git a/bitbake/lib/bb/ui/crumbs/runningbuild.py b/bitbake/lib/bb/ui/crumbs/runningbuild.py
new file mode 100644
index 0000000000..abd3300149
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/runningbuild.py
@@ -0,0 +1,533 @@
1
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2008 Intel Corporation
6#
7# Authored by Rob Bradford <rob@linux.intel.com>
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22import gtk
23import gobject
24import logging
25import time
26import urllib
27import urllib2
28import pango
29from bb.ui.crumbs.hobcolor import HobColors
30from bb.ui.crumbs.hobwidget import HobWarpCellRendererText, HobCellRendererPixbuf
31
32class RunningBuildModel (gtk.TreeStore):
33 (COL_LOG, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_COLOR, COL_NUM_ACTIVE) = range(7)
34
35 def __init__ (self):
36 gtk.TreeStore.__init__ (self,
37 gobject.TYPE_STRING,
38 gobject.TYPE_STRING,
39 gobject.TYPE_STRING,
40 gobject.TYPE_STRING,
41 gobject.TYPE_STRING,
42 gobject.TYPE_STRING,
43 gobject.TYPE_INT)
44
45 def failure_model_filter(self, model, it):
46 color = model.get(it, self.COL_COLOR)[0]
47 if not color:
48 return False
49 if color == HobColors.ERROR or color == HobColors.WARNING:
50 return True
51 return False
52
53 def failure_model(self):
54 model = self.filter_new()
55 model.set_visible_func(self.failure_model_filter)
56 return model
57
58 def foreach_cell_func(self, model, path, iter, usr_data=None):
59 if model.get_value(iter, self.COL_ICON) == "gtk-execute":
60 model.set(iter, self.COL_ICON, "")
61
62 def close_task_refresh(self):
63 self.foreach(self.foreach_cell_func, None)
64
65class RunningBuild (gobject.GObject):
66 __gsignals__ = {
67 'build-started' : (gobject.SIGNAL_RUN_LAST,
68 gobject.TYPE_NONE,
69 ()),
70 'build-succeeded' : (gobject.SIGNAL_RUN_LAST,
71 gobject.TYPE_NONE,
72 ()),
73 'build-failed' : (gobject.SIGNAL_RUN_LAST,
74 gobject.TYPE_NONE,
75 ()),
76 'build-complete' : (gobject.SIGNAL_RUN_LAST,
77 gobject.TYPE_NONE,
78 ()),
79 'build-aborted' : (gobject.SIGNAL_RUN_LAST,
80 gobject.TYPE_NONE,
81 ()),
82 'task-started' : (gobject.SIGNAL_RUN_LAST,
83 gobject.TYPE_NONE,
84 (gobject.TYPE_PYOBJECT,)),
85 'log-error' : (gobject.SIGNAL_RUN_LAST,
86 gobject.TYPE_NONE,
87 ()),
88 'log-warning' : (gobject.SIGNAL_RUN_LAST,
89 gobject.TYPE_NONE,
90 ()),
91 'disk-full' : (gobject.SIGNAL_RUN_LAST,
92 gobject.TYPE_NONE,
93 ()),
94 'no-provider' : (gobject.SIGNAL_RUN_LAST,
95 gobject.TYPE_NONE,
96 (gobject.TYPE_PYOBJECT,)),
97 'log' : (gobject.SIGNAL_RUN_LAST,
98 gobject.TYPE_NONE,
99 (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,)),
100 }
101 pids_to_task = {}
102 tasks_to_iter = {}
103
104 def __init__ (self, sequential=False):
105 gobject.GObject.__init__ (self)
106 self.model = RunningBuildModel()
107 self.sequential = sequential
108 self.buildaborted = False
109
110 def reset (self):
111 self.pids_to_task.clear()
112 self.tasks_to_iter.clear()
113 self.model.clear()
114
115 def handle_event (self, event, pbar=None):
116 # Handle an event from the event queue, this may result in updating
117 # the model and thus the UI. Or it may be to tell us that the build
118 # has finished successfully (or not, as the case may be.)
119
120 parent = None
121 pid = 0
122 package = None
123 task = None
124
125 # If we have a pid attached to this message/event try and get the
126 # (package, task) pair for it. If we get that then get the parent iter
127 # for the message.
128 if hasattr(event, 'pid'):
129 pid = event.pid
130 if hasattr(event, 'process'):
131 pid = event.process
132
133 if pid and pid in self.pids_to_task:
134 (package, task) = self.pids_to_task[pid]
135 parent = self.tasks_to_iter[(package, task)]
136
137 if(isinstance(event, logging.LogRecord)):
138 if event.taskpid == 0 or event.levelno > logging.INFO:
139 self.emit("log", "handle", event)
140 # FIXME: this is a hack! More info in Yocto #1433
141 # http://bugzilla.pokylinux.org/show_bug.cgi?id=1433, temporarily
142 # mask the error message as it's not informative for the user.
143 if event.msg.startswith("Execution of event handler 'run_buildstats' failed"):
144 return
145
146 if (event.levelno < logging.INFO or
147 event.msg.startswith("Running task")):
148 return # don't add these to the list
149
150 if event.levelno >= logging.ERROR:
151 icon = "dialog-error"
152 color = HobColors.ERROR
153 self.emit("log-error")
154 elif event.levelno >= logging.WARNING:
155 icon = "dialog-warning"
156 color = HobColors.WARNING
157 self.emit("log-warning")
158 else:
159 icon = None
160 color = HobColors.OK
161
162 # if we know which package we belong to, we'll append onto its list.
163 # otherwise, we'll jump to the top of the master list
164 if self.sequential or not parent:
165 tree_add = self.model.append
166 else:
167 tree_add = self.model.prepend
168 tree_add(parent,
169 (None,
170 package,
171 task,
172 event.getMessage(),
173 icon,
174 color,
175 0))
176
177 elif isinstance(event, bb.build.TaskStarted):
178 (package, task) = (event._package, event._task)
179
180 # Save out this PID.
181 self.pids_to_task[pid] = (package, task)
182
183 # Check if we already have this package in our model. If so then
184 # that can be the parent for the task. Otherwise we create a new
185 # top level for the package.
186 if ((package, None) in self.tasks_to_iter):
187 parent = self.tasks_to_iter[(package, None)]
188 else:
189 if self.sequential:
190 add = self.model.append
191 else:
192 add = self.model.prepend
193 parent = add(None, (None,
194 package,
195 None,
196 "Package: %s" % (package),
197 None,
198 HobColors.OK,
199 0))
200 self.tasks_to_iter[(package, None)] = parent
201
202 # Because this parent package now has an active child mark it as
203 # such.
204 # @todo if parent is already in error, don't mark it green
205 self.model.set(parent, self.model.COL_ICON, "gtk-execute",
206 self.model.COL_COLOR, HobColors.RUNNING)
207
208 # Add an entry in the model for this task
209 i = self.model.append (parent, (None,
210 package,
211 task,
212 "Task: %s" % (task),
213 "gtk-execute",
214 HobColors.RUNNING,
215 0))
216
217 # update the parent's active task count
218 num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] + 1
219 self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
220
221 # Save out the iter so that we can find it when we have a message
222 # that we need to attach to a task.
223 self.tasks_to_iter[(package, task)] = i
224
225 elif isinstance(event, bb.build.TaskBase):
226 self.emit("log", "info", event._message)
227 current = self.tasks_to_iter[(package, task)]
228 parent = self.tasks_to_iter[(package, None)]
229
230 # remove this task from the parent's active count
231 num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] - 1
232 self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
233
234 if isinstance(event, bb.build.TaskFailed):
235 # Mark the task and parent as failed
236 icon = "dialog-error"
237 color = HobColors.ERROR
238
239 logfile = event.logfile
240 if logfile and os.path.exists(logfile):
241 with open(logfile) as f:
242 logdata = f.read()
243 self.model.append(current, ('pastebin', None, None, logdata, 'gtk-error', HobColors.OK, 0))
244
245 for i in (current, parent):
246 self.model.set(i, self.model.COL_ICON, icon,
247 self.model.COL_COLOR, color)
248 else:
249 icon = None
250 color = HobColors.OK
251
252 # Mark the task as inactive
253 self.model.set(current, self.model.COL_ICON, icon,
254 self.model.COL_COLOR, color)
255
256 # Mark the parent package as inactive, but make sure to
257 # preserve error and active states
258 i = self.tasks_to_iter[(package, None)]
259 if self.model.get(parent, self.model.COL_ICON) != 'dialog-error':
260 self.model.set(parent, self.model.COL_ICON, icon)
261 if num_active == 0:
262 self.model.set(parent, self.model.COL_COLOR, HobColors.OK)
263
264 # Clear the iters and the pids since when the task goes away the
265 # pid will no longer be used for messages
266 del self.tasks_to_iter[(package, task)]
267 del self.pids_to_task[pid]
268
269 elif isinstance(event, bb.event.BuildStarted):
270
271 self.emit("build-started")
272 self.model.prepend(None, (None,
273 None,
274 None,
275 "Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
276 None,
277 HobColors.OK,
278 0))
279 if pbar:
280 pbar.update(0, self.progress_total)
281 pbar.set_title(bb.event.getName(event))
282
283 elif isinstance(event, bb.event.BuildCompleted):
284 failures = int (event._failures)
285 self.model.prepend(None, (None,
286 None,
287 None,
288 "Build Completed (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
289 None,
290 HobColors.OK,
291 0))
292
293 # Emit the appropriate signal depending on the number of failures
294 if self.buildaborted:
295 self.emit ("build-aborted")
296 self.buildaborted = False
297 elif (failures >= 1):
298 self.emit ("build-failed")
299 else:
300 self.emit ("build-succeeded")
301 # Emit a generic "build-complete" signal for things wishing to
302 # handle when the build is finished
303 self.emit("build-complete")
304 # reset the all cell's icon indicator
305 self.model.close_task_refresh()
306 if pbar:
307 pbar.set_text(event.msg)
308
309 elif isinstance(event, bb.event.DiskFull):
310 self.buildaborted = True
311 self.emit("disk-full")
312
313 elif isinstance(event, bb.command.CommandFailed):
314 self.emit("log", "error", "Command execution failed: %s" % (event.error))
315 if event.error.startswith("Exited with"):
316 # If the command fails with an exit code we're done, emit the
317 # generic signal for the UI to notify the user
318 self.emit("build-complete")
319 # reset the all cell's icon indicator
320 self.model.close_task_refresh()
321
322 elif isinstance(event, bb.event.CacheLoadStarted) and pbar:
323 pbar.set_title("Loading cache")
324 self.progress_total = event.total
325 pbar.update(0, self.progress_total)
326 elif isinstance(event, bb.event.CacheLoadProgress) and pbar:
327 pbar.update(event.current, self.progress_total)
328 elif isinstance(event, bb.event.CacheLoadCompleted) and pbar:
329 pbar.update(self.progress_total, self.progress_total)
330 pbar.hide()
331 elif isinstance(event, bb.event.ParseStarted) and pbar:
332 if event.total == 0:
333 return
334 pbar.set_title("Processing recipes")
335 self.progress_total = event.total
336 pbar.update(0, self.progress_total)
337 elif isinstance(event, bb.event.ParseProgress) and pbar:
338 pbar.update(event.current, self.progress_total)
339 elif isinstance(event, bb.event.ParseCompleted) and pbar:
340 pbar.hide()
341 #using runqueue events as many as possible to update the progress bar
342 elif isinstance(event, bb.runqueue.runQueueTaskFailed):
343 self.emit("log", "error", "Task %s (%s) failed with exit code '%s'" % (event.taskid, event.taskstring, event.exitcode))
344 elif isinstance(event, bb.runqueue.sceneQueueTaskFailed):
345 self.emit("log", "warn", "Setscene task %s (%s) failed with exit code '%s' - real task will be run instead" \
346 % (event.taskid, event.taskstring, event.exitcode))
347 elif isinstance(event, (bb.runqueue.runQueueTaskStarted, bb.runqueue.sceneQueueTaskStarted)):
348 if isinstance(event, bb.runqueue.sceneQueueTaskStarted):
349 self.emit("log", "info", "Running setscene task %d of %d (%s)" % \
350 (event.stats.completed + event.stats.active + event.stats.failed + 1,
351 event.stats.total, event.taskstring))
352 else:
353 if event.noexec:
354 tasktype = 'noexec task'
355 else:
356 tasktype = 'task'
357 self.emit("log", "info", "Running %s %s of %s (ID: %s, %s)" % \
358 (tasktype, event.stats.completed + event.stats.active + event.stats.failed + 1,
359 event.stats.total, event.taskid, event.taskstring))
360 message = {}
361 message["eventname"] = bb.event.getName(event)
362 num_of_completed = event.stats.completed + event.stats.failed
363 message["current"] = num_of_completed
364 message["total"] = event.stats.total
365 message["title"] = ""
366 message["task"] = event.taskstring
367 self.emit("task-started", message)
368 elif isinstance(event, bb.event.MultipleProviders):
369 self.emit("log", "info", "multiple providers are available for %s%s (%s)" \
370 % (event._is_runtime and "runtime " or "", event._item, ", ".join(event._candidates)))
371 self.emit("log", "info", "consider defining a PREFERRED_PROVIDER entry to match %s" % (event._item))
372 elif isinstance(event, bb.event.NoProvider):
373 msg = ""
374 if event._runtime:
375 r = "R"
376 else:
377 r = ""
378
379 extra = ''
380 if not event._reasons:
381 if event._close_matches:
382 extra = ". Close matches:\n %s" % '\n '.join(event._close_matches)
383
384 if event._dependees:
385 msg = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s\n" % (r, event._item, ", ".join(event._dependees), r, extra)
386 else:
387 msg = "Nothing %sPROVIDES '%s'%s\n" % (r, event._item, extra)
388 if event._reasons:
389 for reason in event._reasons:
390 msg += ("%s\n" % reason)
391 self.emit("no-provider", msg)
392 self.emit("log", "error", msg)
393 elif isinstance(event, bb.event.LogExecTTY):
394 icon = "dialog-warning"
395 color = HobColors.WARNING
396 if self.sequential or not parent:
397 tree_add = self.model.append
398 else:
399 tree_add = self.model.prepend
400 tree_add(parent,
401 (None,
402 package,
403 task,
404 event.msg,
405 icon,
406 color,
407 0))
408 else:
409 if not isinstance(event, (bb.event.BuildBase,
410 bb.event.StampUpdate,
411 bb.event.ConfigParsed,
412 bb.event.RecipeParsed,
413 bb.event.RecipePreFinalise,
414 bb.runqueue.runQueueEvent,
415 bb.runqueue.runQueueExitWait,
416 bb.event.OperationStarted,
417 bb.event.OperationCompleted,
418 bb.event.OperationProgress)):
419 self.emit("log", "error", "Unknown event: %s" % (event.error if hasattr(event, 'error') else 'error'))
420
421 return
422
423
424def do_pastebin(text):
425 url = 'http://pastebin.com/api_public.php'
426 params = {'paste_code': text, 'paste_format': 'text'}
427
428 req = urllib2.Request(url, urllib.urlencode(params))
429 response = urllib2.urlopen(req)
430 paste_url = response.read()
431
432 return paste_url
433
434
435class RunningBuildTreeView (gtk.TreeView):
436 __gsignals__ = {
437 "button_press_event" : "override"
438 }
439 def __init__ (self, readonly=False, hob=False):
440 gtk.TreeView.__init__ (self)
441 self.readonly = readonly
442
443 # The icon that indicates whether we're building or failed.
444 # add 'hob' flag because there has not only hob to share this code
445 if hob:
446 renderer = HobCellRendererPixbuf ()
447 else:
448 renderer = gtk.CellRendererPixbuf()
449 col = gtk.TreeViewColumn ("Status", renderer)
450 col.add_attribute (renderer, "icon-name", 4)
451 self.append_column (col)
452
453 # The message of the build.
454 # add 'hob' flag because there has not only hob to share this code
455 if hob:
456 self.message_renderer = HobWarpCellRendererText (col_number=1)
457 else:
458 self.message_renderer = gtk.CellRendererText ()
459 self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=3)
460 self.message_column.add_attribute(self.message_renderer, 'background', 5)
461 self.message_renderer.set_property('editable', (not self.readonly))
462 self.append_column (self.message_column)
463
464 def do_button_press_event(self, event):
465 gtk.TreeView.do_button_press_event(self, event)
466
467 if event.button == 3:
468 selection = super(RunningBuildTreeView, self).get_selection()
469 (model, it) = selection.get_selected()
470 if it is not None:
471 can_paste = model.get(it, model.COL_LOG)[0]
472 if can_paste == 'pastebin':
473 # build a simple menu with a pastebin option
474 menu = gtk.Menu()
475 menuitem = gtk.MenuItem("Copy")
476 menu.append(menuitem)
477 menuitem.connect("activate", self.clipboard_handler, (model, it))
478 menuitem.show()
479 menuitem = gtk.MenuItem("Send log to pastebin")
480 menu.append(menuitem)
481 menuitem.connect("activate", self.pastebin_handler, (model, it))
482 menuitem.show()
483 menu.show()
484 menu.popup(None, None, None, event.button, event.time)
485
486 def _add_to_clipboard(self, clipping):
487 """
488 Add the contents of clipping to the system clipboard.
489 """
490 clipboard = gtk.clipboard_get()
491 clipboard.set_text(clipping)
492 clipboard.store()
493
494 def pastebin_handler(self, widget, data):
495 """
496 Send the log data to pastebin, then add the new paste url to the
497 clipboard.
498 """
499 (model, it) = data
500 paste_url = do_pastebin(model.get(it, model.COL_MESSAGE)[0])
501
502 # @todo Provide visual feedback to the user that it is done and that
503 # it worked.
504 print paste_url
505
506 self._add_to_clipboard(paste_url)
507
508 def clipboard_handler(self, widget, data):
509 """
510 """
511 (model, it) = data
512 message = model.get(it, model.COL_MESSAGE)[0]
513
514 self._add_to_clipboard(message)
515
516class BuildFailureTreeView(gtk.TreeView):
517
518 def __init__ (self):
519 gtk.TreeView.__init__(self)
520 self.set_rules_hint(False)
521 self.set_headers_visible(False)
522 self.get_selection().set_mode(gtk.SELECTION_SINGLE)
523
524 # The icon that indicates whether we're building or failed.
525 renderer = HobCellRendererPixbuf ()
526 col = gtk.TreeViewColumn ("Status", renderer)
527 col.add_attribute (renderer, "icon-name", RunningBuildModel.COL_ICON)
528 self.append_column (col)
529
530 # The message of the build.
531 self.message_renderer = HobWarpCellRendererText (col_number=1)
532 self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=RunningBuildModel.COL_MESSAGE, background=RunningBuildModel.COL_COLOR)
533 self.append_column (self.message_column)
diff --git a/bitbake/lib/bb/ui/crumbs/sanitycheckpage.py b/bitbake/lib/bb/ui/crumbs/sanitycheckpage.py
new file mode 100644
index 0000000000..76ce2ecc23
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/sanitycheckpage.py
@@ -0,0 +1,85 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2012 Intel Corporation
6#
7# Authored by Bogdan Marinescu <bogdan.a.marinescu@intel.com>
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22import gtk, gobject
23from bb.ui.crumbs.progressbar import HobProgressBar
24from bb.ui.crumbs.hobwidget import hic
25from bb.ui.crumbs.hobpages import HobPage
26
27#
28# SanityCheckPage
29#
30class SanityCheckPage (HobPage):
31
32 def __init__(self, builder):
33 super(SanityCheckPage, self).__init__(builder)
34 self.running = False
35 self.create_visual_elements()
36 self.show_all()
37
38 def make_label(self, text, bold=True):
39 label = gtk.Label()
40 label.set_alignment(0.0, 0.5)
41 mark = "<span %s>%s</span>" % (self.span_tag('x-large', 'bold') if bold else self.span_tag('medium'), text)
42 label.set_markup(mark)
43 return label
44
45 def start(self):
46 if not self.running:
47 self.running = True
48 gobject.timeout_add(100, self.timer_func)
49
50 def stop(self):
51 self.running = False
52
53 def is_running(self):
54 return self.running
55
56 def timer_func(self):
57 self.progress_bar.pulse()
58 return self.running
59
60 def create_visual_elements(self):
61 # Table'd layout. 'rows' and 'cols' give the table size
62 rows, cols = 30, 50
63 self.table = gtk.Table(rows, cols, True)
64 self.pack_start(self.table, expand=False, fill=False)
65 sx, sy = 2, 2
66 # 'info' icon
67 image = gtk.Image()
68 image.set_from_file(hic.ICON_INFO_DISPLAY_FILE)
69 self.table.attach(image, sx, sx + 2, sy, sy + 3 )
70 image.show()
71 # 'Checking' message
72 label = self.make_label('Hob is checking for correct build system setup')
73 self.table.attach(label, sx + 2, cols, sy, sy + 3, xpadding=5 )
74 label.show()
75 # 'Shouldn't take long' message.
76 label = self.make_label("The check shouldn't take long.", False)
77 self.table.attach(label, sx + 2, cols, sy + 3, sy + 4, xpadding=5)
78 label.show()
79 # Progress bar
80 self.progress_bar = HobProgressBar()
81 self.table.attach(self.progress_bar, sx + 2, cols - 3, sy + 5, sy + 7, xpadding=5)
82 self.progress_bar.show()
83 # All done
84 self.table.show()
85
diff --git a/bitbake/lib/bb/ui/crumbs/utils.py b/bitbake/lib/bb/ui/crumbs/utils.py
new file mode 100644
index 0000000000..939864fa6f
--- /dev/null
+++ b/bitbake/lib/bb/ui/crumbs/utils.py
@@ -0,0 +1,34 @@
1#
2# BitBake UI Utils
3#
4# Copyright (C) 2012 Intel Corporation
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19# This utility method looks for xterm or vte and return the
20# frist to exist, currently we are keeping this simple, but
21# we will likely move the oe.terminal implementation into
22# bitbake which will allow more flexibility.
23
24import os
25import bb
26
27def which_terminal():
28 term = bb.utils.which(os.environ["PATH"], "xterm")
29 if term:
30 return term + " -e "
31 term = bb.utils.which(os.environ["PATH"], "vte")
32 if term:
33 return term + " -c "
34 return None
diff --git a/bitbake/lib/bb/ui/depexp.py b/bitbake/lib/bb/ui/depexp.py
new file mode 100644
index 0000000000..0b160e2f4e
--- /dev/null
+++ b/bitbake/lib/bb/ui/depexp.py
@@ -0,0 +1,326 @@
1#
2# BitBake Graphical GTK based Dependency Explorer
3#
4# Copyright (C) 2007 Ross Burton
5# Copyright (C) 2007 - 2008 Richard Purdie
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20import gobject
21import gtk
22import Queue
23import threading
24import xmlrpclib
25import bb
26import bb.event
27from bb.ui.crumbs.progressbar import HobProgressBar
28
29# Package Model
30(COL_PKG_NAME) = (0)
31
32# Dependency Model
33(TYPE_DEP, TYPE_RDEP) = (0, 1)
34(COL_DEP_TYPE, COL_DEP_PARENT, COL_DEP_PACKAGE) = (0, 1, 2)
35
36
37class PackageDepView(gtk.TreeView):
38 def __init__(self, model, dep_type, label):
39 gtk.TreeView.__init__(self)
40 self.current = None
41 self.dep_type = dep_type
42 self.filter_model = model.filter_new()
43 self.filter_model.set_visible_func(self._filter)
44 self.set_model(self.filter_model)
45 #self.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE)
46 self.append_column(gtk.TreeViewColumn(label, gtk.CellRendererText(), text=COL_DEP_PACKAGE))
47
48 def _filter(self, model, iter):
49 (this_type, package) = model.get(iter, COL_DEP_TYPE, COL_DEP_PARENT)
50 if this_type != self.dep_type: return False
51 return package == self.current
52
53 def set_current_package(self, package):
54 self.current = package
55 self.filter_model.refilter()
56
57
58class PackageReverseDepView(gtk.TreeView):
59 def __init__(self, model, label):
60 gtk.TreeView.__init__(self)
61 self.current = None
62 self.filter_model = model.filter_new()
63 self.filter_model.set_visible_func(self._filter)
64 self.set_model(self.filter_model)
65 self.append_column(gtk.TreeViewColumn(label, gtk.CellRendererText(), text=COL_DEP_PARENT))
66
67 def _filter(self, model, iter):
68 package = model.get_value(iter, COL_DEP_PACKAGE)
69 return package == self.current
70
71 def set_current_package(self, package):
72 self.current = package
73 self.filter_model.refilter()
74
75
76class DepExplorer(gtk.Window):
77 def __init__(self):
78 gtk.Window.__init__(self)
79 self.set_title("Dependency Explorer")
80 self.set_default_size(500, 500)
81 self.connect("delete-event", gtk.main_quit)
82
83 # Create the data models
84 self.pkg_model = gtk.ListStore(gobject.TYPE_STRING)
85 self.pkg_model.set_sort_column_id(COL_PKG_NAME, gtk.SORT_ASCENDING)
86 self.depends_model = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING)
87 self.depends_model.set_sort_column_id(COL_DEP_PACKAGE, gtk.SORT_ASCENDING)
88
89 pane = gtk.HPaned()
90 pane.set_position(250)
91 self.add(pane)
92
93 # The master list of packages
94 scrolled = gtk.ScrolledWindow()
95 scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
96 scrolled.set_shadow_type(gtk.SHADOW_IN)
97
98 self.pkg_treeview = gtk.TreeView(self.pkg_model)
99 self.pkg_treeview.get_selection().connect("changed", self.on_cursor_changed)
100 column = gtk.TreeViewColumn("Package", gtk.CellRendererText(), text=COL_PKG_NAME)
101 self.pkg_treeview.append_column(column)
102 pane.add1(scrolled)
103 scrolled.add(self.pkg_treeview)
104
105 box = gtk.VBox(homogeneous=True, spacing=4)
106
107 # Runtime Depends
108 scrolled = gtk.ScrolledWindow()
109 scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
110 scrolled.set_shadow_type(gtk.SHADOW_IN)
111 self.rdep_treeview = PackageDepView(self.depends_model, TYPE_RDEP, "Runtime Depends")
112 self.rdep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE)
113 scrolled.add(self.rdep_treeview)
114 box.add(scrolled)
115
116 # Build Depends
117 scrolled = gtk.ScrolledWindow()
118 scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
119 scrolled.set_shadow_type(gtk.SHADOW_IN)
120 self.dep_treeview = PackageDepView(self.depends_model, TYPE_DEP, "Build Depends")
121 self.dep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE)
122 scrolled.add(self.dep_treeview)
123 box.add(scrolled)
124 pane.add2(box)
125
126 # Reverse Depends
127 scrolled = gtk.ScrolledWindow()
128 scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
129 scrolled.set_shadow_type(gtk.SHADOW_IN)
130 self.revdep_treeview = PackageReverseDepView(self.depends_model, "Reverse Depends")
131 self.revdep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PARENT)
132 scrolled.add(self.revdep_treeview)
133 box.add(scrolled)
134 pane.add2(box)
135
136 self.show_all()
137
138 def on_package_activated(self, treeview, path, column, data_col):
139 model = treeview.get_model()
140 package = model.get_value(model.get_iter(path), data_col)
141
142 pkg_path = []
143 def finder(model, path, iter, needle):
144 package = model.get_value(iter, COL_PKG_NAME)
145 if package == needle:
146 pkg_path.append(path)
147 return True
148 else:
149 return False
150 self.pkg_model.foreach(finder, package)
151 if pkg_path:
152 self.pkg_treeview.get_selection().select_path(pkg_path[0])
153 self.pkg_treeview.scroll_to_cell(pkg_path[0])
154
155 def on_cursor_changed(self, selection):
156 (model, it) = selection.get_selected()
157 if it is None:
158 current_package = None
159 else:
160 current_package = model.get_value(it, COL_PKG_NAME)
161 self.rdep_treeview.set_current_package(current_package)
162 self.dep_treeview.set_current_package(current_package)
163 self.revdep_treeview.set_current_package(current_package)
164
165
166 def parse(self, depgraph):
167 for package in depgraph["pn"]:
168 self.pkg_model.insert(0, (package,))
169
170 for package in depgraph["depends"]:
171 for depend in depgraph["depends"][package]:
172 self.depends_model.insert (0, (TYPE_DEP, package, depend))
173
174 for package in depgraph["rdepends-pn"]:
175 for rdepend in depgraph["rdepends-pn"][package]:
176 self.depends_model.insert (0, (TYPE_RDEP, package, rdepend))
177
178
179class gtkthread(threading.Thread):
180 quit = threading.Event()
181 def __init__(self, shutdown):
182 threading.Thread.__init__(self)
183 self.setDaemon(True)
184 self.shutdown = shutdown
185
186 def run(self):
187 gobject.threads_init()
188 gtk.gdk.threads_init()
189 gtk.main()
190 gtkthread.quit.set()
191
192
193def main(server, eventHandler, params):
194 try:
195 params.updateFromServer(server)
196 cmdline = params.parseActions()
197 if not cmdline:
198 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
199 return 1
200 if 'msg' in cmdline and cmdline['msg']:
201 logger.error(cmdline['msg'])
202 return 1
203 cmdline = cmdline['action']
204 if not cmdline or cmdline[0] != "generateDotGraph":
205 print("This UI is only compatible with the -g option")
206 return 1
207 ret, error = server.runCommand(["generateDepTreeEvent", cmdline[1], cmdline[2]])
208 if error:
209 print("Error running command '%s': %s" % (cmdline, error))
210 return 1
211 elif ret != True:
212 print("Error running command '%s': returned %s" % (cmdline, ret))
213 return 1
214 except xmlrpclib.Fault as x:
215 print("XMLRPC Fault getting commandline:\n %s" % x)
216 return
217
218 shutdown = 0
219
220 gtkgui = gtkthread(shutdown)
221 gtkgui.start()
222
223 gtk.gdk.threads_enter()
224 dep = DepExplorer()
225 bardialog = gtk.Dialog(parent=dep,
226 flags=gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT)
227 bardialog.set_default_size(400, 50)
228 pbar = HobProgressBar()
229 bardialog.vbox.pack_start(pbar)
230 bardialog.show_all()
231 bardialog.connect("delete-event", gtk.main_quit)
232 gtk.gdk.threads_leave()
233
234 progress_total = 0
235 while True:
236 try:
237 event = eventHandler.waitEvent(0.25)
238 if gtkthread.quit.isSet():
239 _, error = server.runCommand(["stateStop"])
240 if error:
241 print('Unable to cleanly stop: %s' % error)
242 break
243
244 if event is None:
245 continue
246
247 if isinstance(event, bb.event.CacheLoadStarted):
248 progress_total = event.total
249 gtk.gdk.threads_enter()
250 bardialog.set_title("Loading Cache")
251 pbar.update(0)
252 gtk.gdk.threads_leave()
253
254 if isinstance(event, bb.event.CacheLoadProgress):
255 x = event.current
256 gtk.gdk.threads_enter()
257 pbar.update(x * 1.0 / progress_total)
258 pbar.set_title('')
259 gtk.gdk.threads_leave()
260 continue
261
262 if isinstance(event, bb.event.CacheLoadCompleted):
263 bardialog.hide()
264 continue
265
266 if isinstance(event, bb.event.ParseStarted):
267 progress_total = event.total
268 if progress_total == 0:
269 continue
270 gtk.gdk.threads_enter()
271 pbar.update(0)
272 bardialog.set_title("Processing recipes")
273
274 gtk.gdk.threads_leave()
275
276 if isinstance(event, bb.event.ParseProgress):
277 x = event.current
278 gtk.gdk.threads_enter()
279 pbar.update(x * 1.0 / progress_total)
280 pbar.set_title('')
281 gtk.gdk.threads_leave()
282 continue
283
284 if isinstance(event, bb.event.ParseCompleted):
285 bardialog.hide()
286 continue
287
288 if isinstance(event, bb.event.DepTreeGenerated):
289 gtk.gdk.threads_enter()
290 dep.parse(event._depgraph)
291 gtk.gdk.threads_leave()
292
293 if isinstance(event, bb.command.CommandCompleted):
294 continue
295
296 if isinstance(event, bb.command.CommandFailed):
297 print("Command execution failed: %s" % event.error)
298 return event.exitcode
299
300 if isinstance(event, bb.command.CommandExit):
301 return event.exitcode
302
303 if isinstance(event, bb.cooker.CookerExit):
304 break
305
306 continue
307 except EnvironmentError as ioerror:
308 # ignore interrupted io
309 if ioerror.args[0] == 4:
310 pass
311 except KeyboardInterrupt:
312 if shutdown == 2:
313 print("\nThird Keyboard Interrupt, exit.\n")
314 break
315 if shutdown == 1:
316 print("\nSecond Keyboard Interrupt, stopping...\n")
317 _, error = server.runCommand(["stateForceShutdown"])
318 if error:
319 print('Unable to cleanly stop: %s' % error)
320 if shutdown == 0:
321 print("\nKeyboard Interrupt, closing down...\n")
322 _, error = server.runCommand(["stateShutdown"])
323 if error:
324 print('Unable to cleanly shutdown: %s' % error)
325 shutdown = shutdown + 1
326 pass
diff --git a/bitbake/lib/bb/ui/goggle.py b/bitbake/lib/bb/ui/goggle.py
new file mode 100644
index 0000000000..f4ee7b41ae
--- /dev/null
+++ b/bitbake/lib/bb/ui/goggle.py
@@ -0,0 +1,121 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2008 Intel Corporation
5#
6# Authored by Rob Bradford <rob@linux.intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import gobject
22import gtk
23import xmlrpclib
24from bb.ui.crumbs.runningbuild import RunningBuildTreeView, RunningBuild
25from bb.ui.crumbs.progress import ProgressBar
26
27import Queue
28
29
30def event_handle_idle_func (eventHandler, build, pbar):
31
32 # Consume as many messages as we can in the time available to us
33 event = eventHandler.getEvent()
34 while event:
35 build.handle_event (event, pbar)
36 event = eventHandler.getEvent()
37
38 return True
39
40def scroll_tv_cb (model, path, iter, view):
41 view.scroll_to_cell (path)
42
43
44# @todo hook these into the GUI so the user has feedback...
45def running_build_failed_cb (running_build):
46 pass
47
48
49def running_build_succeeded_cb (running_build):
50 pass
51
52
53class MainWindow (gtk.Window):
54 def __init__ (self):
55 gtk.Window.__init__ (self, gtk.WINDOW_TOPLEVEL)
56
57 # Setup tree view and the scrolled window
58 scrolled_window = gtk.ScrolledWindow ()
59 self.add (scrolled_window)
60 self.cur_build_tv = RunningBuildTreeView()
61 self.connect("delete-event", gtk.main_quit)
62 self.set_default_size(640, 480)
63 scrolled_window.add (self.cur_build_tv)
64
65
66def main (server, eventHandler, params):
67 gobject.threads_init()
68 gtk.gdk.threads_init()
69
70 window = MainWindow ()
71 window.show_all ()
72 pbar = ProgressBar(window)
73 pbar.connect("delete-event", gtk.main_quit)
74
75 # Create the object for the current build
76 running_build = RunningBuild ()
77 window.cur_build_tv.set_model (running_build.model)
78 running_build.model.connect("row-inserted", scroll_tv_cb, window.cur_build_tv)
79 running_build.connect ("build-succeeded", running_build_succeeded_cb)
80 running_build.connect ("build-failed", running_build_failed_cb)
81
82 try:
83 params.updateFromServer(server)
84 cmdline = params.parseActions()
85 if not cmdline:
86 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
87 return 1
88 if 'msg' in cmdline and cmdline['msg']:
89 logger.error(cmdline['msg'])
90 return 1
91 cmdline = cmdline['action']
92 ret, error = server.runCommand(cmdline)
93 if error:
94 print("Error running command '%s': %s" % (cmdline, error))
95 return 1
96 elif ret != True:
97 print("Error running command '%s': returned %s" % (cmdline, ret))
98 return 1
99 except xmlrpclib.Fault as x:
100 print("XMLRPC Fault getting commandline:\n %s" % x)
101 return 1
102
103 # Use a timeout function for probing the event queue to find out if we
104 # have a message waiting for us.
105 gobject.timeout_add (100,
106 event_handle_idle_func,
107 eventHandler,
108 running_build,
109 pbar)
110
111 try:
112 gtk.main()
113 except EnvironmentError as ioerror:
114 # ignore interrupted io
115 if ioerror.args[0] == 4:
116 pass
117 except KeyboardInterrupt:
118 pass
119 finally:
120 server.runCommand(["stateForceShutdown"])
121
diff --git a/bitbake/lib/bb/ui/hob.py b/bitbake/lib/bb/ui/hob.py
new file mode 100755
index 0000000000..154a3b3b4a
--- /dev/null
+++ b/bitbake/lib/bb/ui/hob.py
@@ -0,0 +1,109 @@
1#!/usr/bin/env python
2#
3# BitBake Graphical GTK User Interface
4#
5# Copyright (C) 2011 Intel Corporation
6#
7# Authored by Joshua Lock <josh@linux.intel.com>
8# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License version 2 as
12# published by the Free Software Foundation.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License along
20# with this program; if not, write to the Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
23import sys
24import os
25requirements = "FATAL: Hob requires Gtk+ 2.20.0 or higher, PyGtk 2.21.0 or higher"
26try:
27 import gobject
28 import gtk
29 import pygtk
30 pygtk.require('2.0') # to be certain we don't have gtk+ 1.x !?!
31 gtkver = gtk.gtk_version
32 pygtkver = gtk.pygtk_version
33 if gtkver < (2, 20, 0) or pygtkver < (2, 21, 0):
34 sys.exit("%s,\nYou have Gtk+ %s and PyGtk %s." % (requirements,
35 ".".join(map(str, gtkver)),
36 ".".join(map(str, pygtkver))))
37except ImportError as exc:
38 sys.exit("%s (%s)." % (requirements, str(exc)))
39sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
40try:
41 import bb
42except RuntimeError as exc:
43 sys.exit(str(exc))
44from bb.ui import uihelper
45from bb.ui.crumbs.hoblistmodel import RecipeListModel, PackageListModel
46from bb.ui.crumbs.hobeventhandler import HobHandler
47from bb.ui.crumbs.builder import Builder
48
49featureSet = [bb.cooker.CookerFeatures.HOB_EXTRA_CACHES]
50
51def event_handle_idle_func(eventHandler, hobHandler):
52 # Consume as many messages as we can in the time available to us
53 if not eventHandler:
54 return False
55 event = eventHandler.getEvent()
56 while event:
57 hobHandler.handle_event(event)
58 event = eventHandler.getEvent()
59 return True
60
61_evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord",
62 "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted",
63 "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted",
64 "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed",
65 "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit",
66 "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted",
67 "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed",
68 "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent",
69 "bb.event.SanityCheckPassed", "bb.event.SanityCheckFailed", "bb.event.PackageInfo",
70 "bb.event.TargetsTreeGenerated", "bb.event.ConfigFilesFound", "bb.event.ConfigFilePathFound",
71 "bb.event.FilesMatchingFound", "bb.event.NetworkTestFailed", "bb.event.NetworkTestPassed",
72 "bb.event.BuildStarted", "bb.event.BuildCompleted", "bb.event.DiskFull"]
73
74def main (server, eventHandler, params):
75 params.updateFromServer(server)
76 gobject.threads_init()
77
78 # That indicates whether the Hob and the bitbake server are
79 # running on different machines
80 # recipe model and package model
81 recipe_model = RecipeListModel()
82 package_model = PackageListModel()
83
84 llevel, debug_domains = bb.msg.constructLogOptions()
85 server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list])
86 hobHandler = HobHandler(server, recipe_model, package_model)
87 builder = Builder(hobHandler, recipe_model, package_model)
88
89 # This timeout function regularly probes the event queue to find out if we
90 # have any messages waiting for us.
91 gobject.timeout_add(10, event_handle_idle_func, eventHandler, hobHandler)
92
93 try:
94 gtk.main()
95 except EnvironmentError as ioerror:
96 # ignore interrupted io
97 if ioerror.args[0] == 4:
98 pass
99 finally:
100 hobHandler.cancel_build(force = True)
101
102if __name__ == "__main__":
103 try:
104 ret = main()
105 except Exception:
106 ret = 1
107 import traceback
108 traceback.print_exc(15)
109 sys.exit(ret)
diff --git a/bitbake/lib/bb/ui/icons/images/images_display.png b/bitbake/lib/bb/ui/icons/images/images_display.png
new file mode 100644
index 0000000000..a7f87101af
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/images/images_display.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/images/images_hover.png b/bitbake/lib/bb/ui/icons/images/images_hover.png
new file mode 100644
index 0000000000..2d9cd99b8e
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/images/images_hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/add-hover.png b/bitbake/lib/bb/ui/icons/indicators/add-hover.png
new file mode 100644
index 0000000000..526df770d1
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/add-hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/add.png b/bitbake/lib/bb/ui/icons/indicators/add.png
new file mode 100644
index 0000000000..31e7090d61
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/add.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/alert.png b/bitbake/lib/bb/ui/icons/indicators/alert.png
new file mode 100644
index 0000000000..d1c6f55a2f
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/alert.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/confirmation.png b/bitbake/lib/bb/ui/icons/indicators/confirmation.png
new file mode 100644
index 0000000000..3a5402d1e3
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/confirmation.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/denied.png b/bitbake/lib/bb/ui/icons/indicators/denied.png
new file mode 100644
index 0000000000..ee35c7defa
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/denied.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/error.png b/bitbake/lib/bb/ui/icons/indicators/error.png
new file mode 100644
index 0000000000..d06a8c151a
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/error.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/info.png b/bitbake/lib/bb/ui/icons/indicators/info.png
new file mode 100644
index 0000000000..ee8e8d8462
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/info.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/issues.png b/bitbake/lib/bb/ui/icons/indicators/issues.png
new file mode 100644
index 0000000000..b0c7461334
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/issues.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/refresh.png b/bitbake/lib/bb/ui/icons/indicators/refresh.png
new file mode 100644
index 0000000000..eb6c419db8
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/refresh.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/remove-hover.png b/bitbake/lib/bb/ui/icons/indicators/remove-hover.png
new file mode 100644
index 0000000000..aa57c69982
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/remove-hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/remove.png b/bitbake/lib/bb/ui/icons/indicators/remove.png
new file mode 100644
index 0000000000..05c3c293d4
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/remove.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/indicators/tick.png b/bitbake/lib/bb/ui/icons/indicators/tick.png
new file mode 100644
index 0000000000..beaad361c3
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/indicators/tick.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/info/info_display.png b/bitbake/lib/bb/ui/icons/info/info_display.png
new file mode 100644
index 0000000000..5afbba29f5
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/info/info_display.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/info/info_hover.png b/bitbake/lib/bb/ui/icons/info/info_hover.png
new file mode 100644
index 0000000000..f9d294dfae
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/info/info_hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/layers/layers_display.png b/bitbake/lib/bb/ui/icons/layers/layers_display.png
new file mode 100644
index 0000000000..b7f9053a9e
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/layers/layers_display.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/layers/layers_hover.png b/bitbake/lib/bb/ui/icons/layers/layers_hover.png
new file mode 100644
index 0000000000..0bf3ce0dbc
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/layers/layers_hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/packages/packages_display.png b/bitbake/lib/bb/ui/icons/packages/packages_display.png
new file mode 100644
index 0000000000..f5d0a5064d
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/packages/packages_display.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/packages/packages_hover.png b/bitbake/lib/bb/ui/icons/packages/packages_hover.png
new file mode 100644
index 0000000000..c081165f34
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/packages/packages_hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/recipe/recipe_display.png b/bitbake/lib/bb/ui/icons/recipe/recipe_display.png
new file mode 100644
index 0000000000..e9809bc7d9
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/recipe/recipe_display.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/recipe/recipe_hover.png b/bitbake/lib/bb/ui/icons/recipe/recipe_hover.png
new file mode 100644
index 0000000000..7e48da9af0
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/recipe/recipe_hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/settings/settings_display.png b/bitbake/lib/bb/ui/icons/settings/settings_display.png
new file mode 100644
index 0000000000..88c464db04
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/settings/settings_display.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/settings/settings_hover.png b/bitbake/lib/bb/ui/icons/settings/settings_hover.png
new file mode 100644
index 0000000000..d92a0bf2c3
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/settings/settings_hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/templates/templates_display.png b/bitbake/lib/bb/ui/icons/templates/templates_display.png
new file mode 100644
index 0000000000..153c7afb62
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/templates/templates_display.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/icons/templates/templates_hover.png b/bitbake/lib/bb/ui/icons/templates/templates_hover.png
new file mode 100644
index 0000000000..afb7165fe5
--- /dev/null
+++ b/bitbake/lib/bb/ui/icons/templates/templates_hover.png
Binary files differ
diff --git a/bitbake/lib/bb/ui/knotty.py b/bitbake/lib/bb/ui/knotty.py
new file mode 100644
index 0000000000..c1ee9f5269
--- /dev/null
+++ b/bitbake/lib/bb/ui/knotty.py
@@ -0,0 +1,541 @@
1#
2# BitBake (No)TTY UI Implementation
3#
4# Handling output to TTYs or files (no TTY)
5#
6# Copyright (C) 2006-2012 Richard Purdie
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21from __future__ import division
22
23import os
24import sys
25import xmlrpclib
26import logging
27import progressbar
28import signal
29import bb.msg
30import time
31import fcntl
32import struct
33import copy
34from bb.ui import uihelper
35
36logger = logging.getLogger("BitBake")
37interactive = sys.stdout.isatty()
38
39class BBProgress(progressbar.ProgressBar):
40 def __init__(self, msg, maxval):
41 self.msg = msg
42 widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ',
43 progressbar.ETA()]
44
45 try:
46 self._resize_default = signal.getsignal(signal.SIGWINCH)
47 except:
48 self._resize_default = None
49 progressbar.ProgressBar.__init__(self, maxval, [self.msg + ": "] + widgets)
50
51 def _handle_resize(self, signum, frame):
52 progressbar.ProgressBar._handle_resize(self, signum, frame)
53 if self._resize_default:
54 self._resize_default(signum, frame)
55 def finish(self):
56 progressbar.ProgressBar.finish(self)
57 if self._resize_default:
58 signal.signal(signal.SIGWINCH, self._resize_default)
59
60class NonInteractiveProgress(object):
61 fobj = sys.stdout
62
63 def __init__(self, msg, maxval):
64 self.msg = msg
65 self.maxval = maxval
66
67 def start(self):
68 self.fobj.write("%s..." % self.msg)
69 self.fobj.flush()
70 return self
71
72 def update(self, value):
73 pass
74
75 def finish(self):
76 self.fobj.write("done.\n")
77 self.fobj.flush()
78
79def new_progress(msg, maxval):
80 if interactive:
81 return BBProgress(msg, maxval)
82 else:
83 return NonInteractiveProgress(msg, maxval)
84
85def pluralise(singular, plural, qty):
86 if(qty == 1):
87 return singular % qty
88 else:
89 return plural % qty
90
91
92class InteractConsoleLogFilter(logging.Filter):
93 def __init__(self, tf, format):
94 self.tf = tf
95 self.format = format
96
97 def filter(self, record):
98 if record.levelno == self.format.NOTE and (record.msg.startswith("Running") or record.msg.startswith("recipe ")):
99 return False
100 self.tf.clearFooter()
101 return True
102
103class TerminalFilter(object):
104 columns = 80
105
106 def sigwinch_handle(self, signum, frame):
107 self.columns = self.getTerminalColumns()
108 if self._sigwinch_default:
109 self._sigwinch_default(signum, frame)
110
111 def getTerminalColumns(self):
112 def ioctl_GWINSZ(fd):
113 try:
114 cr = struct.unpack('hh', fcntl.ioctl(fd, self.termios.TIOCGWINSZ, '1234'))
115 except:
116 return None
117 return cr
118 cr = ioctl_GWINSZ(sys.stdout.fileno())
119 if not cr:
120 try:
121 fd = os.open(os.ctermid(), os.O_RDONLY)
122 cr = ioctl_GWINSZ(fd)
123 os.close(fd)
124 except:
125 pass
126 if not cr:
127 try:
128 cr = (env['LINES'], env['COLUMNS'])
129 except:
130 cr = (25, 80)
131 return cr[1]
132
133 def __init__(self, main, helper, console, format):
134 self.main = main
135 self.helper = helper
136 self.cuu = None
137 self.stdinbackup = None
138 self.interactive = sys.stdout.isatty()
139 self.footer_present = False
140 self.lastpids = []
141
142 if not self.interactive:
143 return
144
145 try:
146 import curses
147 except ImportError:
148 sys.exit("FATAL: The knotty ui could not load the required curses python module.")
149
150 import termios
151 self.curses = curses
152 self.termios = termios
153 try:
154 fd = sys.stdin.fileno()
155 self.stdinbackup = termios.tcgetattr(fd)
156 new = copy.deepcopy(self.stdinbackup)
157 new[3] = new[3] & ~termios.ECHO
158 termios.tcsetattr(fd, termios.TCSADRAIN, new)
159 curses.setupterm()
160 if curses.tigetnum("colors") > 2:
161 format.enable_color()
162 self.ed = curses.tigetstr("ed")
163 if self.ed:
164 self.cuu = curses.tigetstr("cuu")
165 try:
166 self._sigwinch_default = signal.getsignal(signal.SIGWINCH)
167 signal.signal(signal.SIGWINCH, self.sigwinch_handle)
168 except:
169 pass
170 self.columns = self.getTerminalColumns()
171 except:
172 self.cuu = None
173 console.addFilter(InteractConsoleLogFilter(self, format))
174
175 def clearFooter(self):
176 if self.footer_present:
177 lines = self.footer_present
178 sys.stdout.write(self.curses.tparm(self.cuu, lines))
179 sys.stdout.write(self.curses.tparm(self.ed))
180 self.footer_present = False
181
182 def updateFooter(self):
183 if not self.cuu:
184 return
185 activetasks = self.helper.running_tasks
186 failedtasks = self.helper.failed_tasks
187 runningpids = self.helper.running_pids
188 if self.footer_present and (self.lastcount == self.helper.tasknumber_current) and (self.lastpids == runningpids):
189 return
190 if self.footer_present:
191 self.clearFooter()
192 if (not self.helper.tasknumber_total or self.helper.tasknumber_current == self.helper.tasknumber_total) and not len(activetasks):
193 return
194 tasks = []
195 for t in runningpids:
196 tasks.append("%s (pid %s)" % (activetasks[t]["title"], t))
197
198 if self.main.shutdown:
199 content = "Waiting for %s running tasks to finish:" % len(activetasks)
200 elif not len(activetasks):
201 content = "No currently running tasks (%s of %s)" % (self.helper.tasknumber_current, self.helper.tasknumber_total)
202 else:
203 content = "Currently %s running tasks (%s of %s):" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total)
204 print(content)
205 lines = 1 + int(len(content) / (self.columns + 1))
206 for tasknum, task in enumerate(tasks):
207 content = "%s: %s" % (tasknum, task)
208 print(content)
209 lines = lines + 1 + int(len(content) / (self.columns + 1))
210 self.footer_present = lines
211 self.lastpids = runningpids[:]
212 self.lastcount = self.helper.tasknumber_current
213
214 def finish(self):
215 if self.stdinbackup:
216 fd = sys.stdin.fileno()
217 self.termios.tcsetattr(fd, self.termios.TCSADRAIN, self.stdinbackup)
218
219def _log_settings_from_server(server):
220 # Get values of variables which control our output
221 includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"])
222 if error:
223 logger.error("Unable to get the value of BBINCLUDELOGS variable: %s" % error)
224 raise BaseException(error)
225 loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"])
226 if error:
227 logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error)
228 raise BaseException(error)
229 consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"])
230 if error:
231 logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error)
232 raise BaseException(error)
233 return includelogs, loglines, consolelogfile
234
235_evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord",
236 "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted",
237 "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted",
238 "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed",
239 "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit",
240 "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted",
241 "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed",
242 "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent"]
243
244def main(server, eventHandler, params, tf = TerminalFilter):
245
246 includelogs, loglines, consolelogfile = _log_settings_from_server(server)
247
248 if sys.stdin.isatty() and sys.stdout.isatty():
249 log_exec_tty = True
250 else:
251 log_exec_tty = False
252
253 helper = uihelper.BBUIHelper()
254
255 console = logging.StreamHandler(sys.stdout)
256 format_str = "%(levelname)s: %(message)s"
257 format = bb.msg.BBLogFormatter(format_str)
258 bb.msg.addDefaultlogFilter(console)
259 console.setFormatter(format)
260 logger.addHandler(console)
261
262 if params.options.remote_server and params.options.kill_server:
263 server.terminateServer()
264 return
265
266 if consolelogfile and not params.options.show_environment:
267 bb.utils.mkdirhier(os.path.dirname(consolelogfile))
268 conlogformat = bb.msg.BBLogFormatter(format_str)
269 consolelog = logging.FileHandler(consolelogfile)
270 bb.msg.addDefaultlogFilter(consolelog)
271 consolelog.setFormatter(conlogformat)
272 logger.addHandler(consolelog)
273
274 llevel, debug_domains = bb.msg.constructLogOptions()
275 server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list])
276
277 if not params.observe_only:
278 params.updateFromServer(server)
279 cmdline = params.parseActions()
280 if not cmdline:
281 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
282 return 1
283 if 'msg' in cmdline and cmdline['msg']:
284 logger.error(cmdline['msg'])
285 return 1
286
287 ret, error = server.runCommand(cmdline['action'])
288 if error:
289 logger.error("Command '%s' failed: %s" % (cmdline, error))
290 return 1
291 elif ret != True:
292 logger.error("Command '%s' failed: returned %s" % (cmdline, ret))
293 return 1
294
295
296 parseprogress = None
297 cacheprogress = None
298 main.shutdown = 0
299 interrupted = False
300 return_value = 0
301 errors = 0
302 warnings = 0
303 taskfailures = []
304
305 termfilter = tf(main, helper, console, format)
306
307 while True:
308 try:
309 event = eventHandler.waitEvent(0)
310 if event is None:
311 termfilter.updateFooter()
312 event = eventHandler.waitEvent(0.25)
313 if event is None:
314 if main.shutdown > 1:
315 break
316 continue
317 helper.eventHandler(event)
318 if isinstance(event, bb.runqueue.runQueueExitWait):
319 if not main.shutdown:
320 main.shutdown = 1
321
322 if isinstance(event, bb.event.LogExecTTY):
323 if log_exec_tty:
324 tries = event.retries
325 while tries:
326 print("Trying to run: %s" % event.prog)
327 if os.system(event.prog) == 0:
328 break
329 time.sleep(event.sleep_delay)
330 tries -= 1
331 if tries:
332 continue
333 logger.warn(event.msg)
334 continue
335
336 if isinstance(event, logging.LogRecord):
337 if event.levelno >= format.ERROR:
338 errors = errors + 1
339 return_value = 1
340 elif event.levelno == format.WARNING:
341 warnings = warnings + 1
342 # For "normal" logging conditions, don't show note logs from tasks
343 # but do show them if the user has changed the default log level to
344 # include verbose/debug messages
345 if event.taskpid != 0 and event.levelno <= format.NOTE:
346 continue
347 logger.handle(event)
348 continue
349
350 if isinstance(event, bb.build.TaskFailed):
351 return_value = 1
352 logfile = event.logfile
353 if logfile and os.path.exists(logfile):
354 termfilter.clearFooter()
355 bb.error("Logfile of failure stored in: %s" % logfile)
356 if includelogs and not event.errprinted:
357 print("Log data follows:")
358 f = open(logfile, "r")
359 lines = []
360 while True:
361 l = f.readline()
362 if l == '':
363 break
364 l = l.rstrip()
365 if loglines:
366 lines.append(' | %s' % l)
367 if len(lines) > int(loglines):
368 lines.pop(0)
369 else:
370 print('| %s' % l)
371 f.close()
372 if lines:
373 for line in lines:
374 print(line)
375 if isinstance(event, bb.build.TaskBase):
376 logger.info(event._message)
377 continue
378 if isinstance(event, bb.event.ParseStarted):
379 if event.total == 0:
380 continue
381 parseprogress = new_progress("Parsing recipes", event.total).start()
382 continue
383 if isinstance(event, bb.event.ParseProgress):
384 parseprogress.update(event.current)
385 continue
386 if isinstance(event, bb.event.ParseCompleted):
387 if not parseprogress:
388 continue
389
390 parseprogress.finish()
391 print(("Parsing of %d .bb files complete (%d cached, %d parsed). %d targets, %d skipped, %d masked, %d errors."
392 % ( event.total, event.cached, event.parsed, event.virtuals, event.skipped, event.masked, event.errors)))
393 continue
394
395 if isinstance(event, bb.event.CacheLoadStarted):
396 cacheprogress = new_progress("Loading cache", event.total).start()
397 continue
398 if isinstance(event, bb.event.CacheLoadProgress):
399 cacheprogress.update(event.current)
400 continue
401 if isinstance(event, bb.event.CacheLoadCompleted):
402 cacheprogress.finish()
403 print("Loaded %d entries from dependency cache." % event.num_entries)
404 continue
405
406 if isinstance(event, bb.command.CommandFailed):
407 return_value = event.exitcode
408 if event.error:
409 errors = errors + 1
410 logger.error("Command execution failed: %s", event.error)
411 main.shutdown = 2
412 continue
413 if isinstance(event, bb.command.CommandExit):
414 if not return_value:
415 return_value = event.exitcode
416 continue
417 if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)):
418 main.shutdown = 2
419 continue
420 if isinstance(event, bb.event.MultipleProviders):
421 logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "",
422 event._item,
423 ", ".join(event._candidates))
424 logger.info("consider defining a PREFERRED_PROVIDER entry to match %s", event._item)
425 continue
426 if isinstance(event, bb.event.NoProvider):
427 return_value = 1
428 errors = errors + 1
429 if event._runtime:
430 r = "R"
431 else:
432 r = ""
433
434 extra = ''
435 if not event._reasons:
436 if event._close_matches:
437 extra = ". Close matches:\n %s" % '\n '.join(event._close_matches)
438
439 if event._dependees:
440 logger.error("Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s", r, event._item, ", ".join(event._dependees), r, extra)
441 else:
442 logger.error("Nothing %sPROVIDES '%s'%s", r, event._item, extra)
443 if event._reasons:
444 for reason in event._reasons:
445 logger.error("%s", reason)
446 continue
447
448 if isinstance(event, bb.runqueue.sceneQueueTaskStarted):
449 logger.info("Running setscene task %d of %d (%s)" % (event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskstring))
450 continue
451
452 if isinstance(event, bb.runqueue.runQueueTaskStarted):
453 if event.noexec:
454 tasktype = 'noexec task'
455 else:
456 tasktype = 'task'
457 logger.info("Running %s %s of %s (ID: %s, %s)",
458 tasktype,
459 event.stats.completed + event.stats.active +
460 event.stats.failed + 1,
461 event.stats.total, event.taskid, event.taskstring)
462 continue
463
464 if isinstance(event, bb.runqueue.runQueueTaskFailed):
465 taskfailures.append(event.taskstring)
466 logger.error("Task %s (%s) failed with exit code '%s'",
467 event.taskid, event.taskstring, event.exitcode)
468 continue
469
470 if isinstance(event, bb.runqueue.sceneQueueTaskFailed):
471 logger.warn("Setscene task %s (%s) failed with exit code '%s' - real task will be run instead",
472 event.taskid, event.taskstring, event.exitcode)
473 continue
474
475 if isinstance(event, bb.event.DepTreeGenerated):
476 continue
477
478 # ignore
479 if isinstance(event, (bb.event.BuildBase,
480 bb.event.MetadataEvent,
481 bb.event.StampUpdate,
482 bb.event.ConfigParsed,
483 bb.event.RecipeParsed,
484 bb.event.RecipePreFinalise,
485 bb.runqueue.runQueueEvent,
486 bb.runqueue.runQueueExitWait,
487 bb.event.OperationStarted,
488 bb.event.OperationCompleted,
489 bb.event.OperationProgress,
490 bb.event.DiskFull)):
491 continue
492
493 logger.error("Unknown event: %s", event)
494
495 except EnvironmentError as ioerror:
496 termfilter.clearFooter()
497 # ignore interrupted io
498 if ioerror.args[0] == 4:
499 pass
500 except KeyboardInterrupt:
501 termfilter.clearFooter()
502 if params.observe_only:
503 print("\nKeyboard Interrupt, exiting observer...")
504 main.shutdown = 2
505 if not params.observe_only and main.shutdown == 1:
506 print("\nSecond Keyboard Interrupt, stopping...\n")
507 _, error = server.runCommand(["stateForceShutdown"])
508 if error:
509 logger.error("Unable to cleanly stop: %s" % error)
510 if not params.observe_only and main.shutdown == 0:
511 print("\nKeyboard Interrupt, closing down...\n")
512 interrupted = True
513 _, error = server.runCommand(["stateShutdown"])
514 if error:
515 logger.error("Unable to cleanly shutdown: %s" % error)
516 main.shutdown = main.shutdown + 1
517 pass
518
519 summary = ""
520 if taskfailures:
521 summary += pluralise("\nSummary: %s task failed:",
522 "\nSummary: %s tasks failed:", len(taskfailures))
523 for failure in taskfailures:
524 summary += "\n %s" % failure
525 if warnings:
526 summary += pluralise("\nSummary: There was %s WARNING message shown.",
527 "\nSummary: There were %s WARNING messages shown.", warnings)
528 if return_value and errors:
529 summary += pluralise("\nSummary: There was %s ERROR message shown, returning a non-zero exit code.",
530 "\nSummary: There were %s ERROR messages shown, returning a non-zero exit code.", errors)
531 if summary:
532 print(summary)
533
534 if interrupted:
535 print("Execution was interrupted, returning a non-zero exit code.")
536 if return_value == 0:
537 return_value = 1
538
539 termfilter.finish()
540
541 return return_value
diff --git a/bitbake/lib/bb/ui/ncurses.py b/bitbake/lib/bb/ui/ncurses.py
new file mode 100644
index 0000000000..b6c20ec388
--- /dev/null
+++ b/bitbake/lib/bb/ui/ncurses.py
@@ -0,0 +1,373 @@
1#
2# BitBake Curses UI Implementation
3#
4# Implements an ncurses frontend for the BitBake utility.
5#
6# Copyright (C) 2006 Michael 'Mickey' Lauer
7# Copyright (C) 2006-2007 Richard Purdie
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22"""
23 We have the following windows:
24
25 1.) Main Window: Shows what we are ultimately building and how far we are. Includes status bar
26 2.) Thread Activity Window: Shows one status line for every concurrent bitbake thread.
27 3.) Command Line Window: Contains an interactive command line where you can interact w/ Bitbake.
28
29 Basic window layout is like that:
30
31 |---------------------------------------------------------|
32 | <Main Window> | <Thread Activity Window> |
33 | | 0: foo do_compile complete|
34 | Building Gtk+-2.6.10 | 1: bar do_patch complete |
35 | Status: 60% | ... |
36 | | ... |
37 | | ... |
38 |---------------------------------------------------------|
39 |<Command Line Window> |
40 |>>> which virtual/kernel |
41 |openzaurus-kernel |
42 |>>> _ |
43 |---------------------------------------------------------|
44
45"""
46
47
48from __future__ import division
49import logging
50import os, sys, itertools, time, subprocess
51
52try:
53 import curses
54except ImportError:
55 sys.exit("FATAL: The ncurses ui could not load the required curses python module.")
56
57import bb
58import xmlrpclib
59from bb import ui
60from bb.ui import uihelper
61
62parsespin = itertools.cycle( r'|/-\\' )
63
64X = 0
65Y = 1
66WIDTH = 2
67HEIGHT = 3
68
69MAXSTATUSLENGTH = 32
70
71class NCursesUI:
72 """
73 NCurses UI Class
74 """
75 class Window:
76 """Base Window Class"""
77 def __init__( self, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ):
78 self.win = curses.newwin( height, width, y, x )
79 self.dimensions = ( x, y, width, height )
80 """
81 if curses.has_colors():
82 color = 1
83 curses.init_pair( color, fg, bg )
84 self.win.bkgdset( ord(' '), curses.color_pair(color) )
85 else:
86 self.win.bkgdset( ord(' '), curses.A_BOLD )
87 """
88 self.erase()
89 self.setScrolling()
90 self.win.noutrefresh()
91
92 def erase( self ):
93 self.win.erase()
94
95 def setScrolling( self, b = True ):
96 self.win.scrollok( b )
97 self.win.idlok( b )
98
99 def setBoxed( self ):
100 self.boxed = True
101 self.win.box()
102 self.win.noutrefresh()
103
104 def setText( self, x, y, text, *args ):
105 self.win.addstr( y, x, text, *args )
106 self.win.noutrefresh()
107
108 def appendText( self, text, *args ):
109 self.win.addstr( text, *args )
110 self.win.noutrefresh()
111
112 def drawHline( self, y ):
113 self.win.hline( y, 0, curses.ACS_HLINE, self.dimensions[WIDTH] )
114 self.win.noutrefresh()
115
116 class DecoratedWindow( Window ):
117 """Base class for windows with a box and a title bar"""
118 def __init__( self, title, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ):
119 NCursesUI.Window.__init__( self, x+1, y+3, width-2, height-4, fg, bg )
120 self.decoration = NCursesUI.Window( x, y, width, height, fg, bg )
121 self.decoration.setBoxed()
122 self.decoration.win.hline( 2, 1, curses.ACS_HLINE, width-2 )
123 self.setTitle( title )
124
125 def setTitle( self, title ):
126 self.decoration.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD )
127
128 #-------------------------------------------------------------------------#
129# class TitleWindow( Window ):
130 #-------------------------------------------------------------------------#
131# """Title Window"""
132# def __init__( self, x, y, width, height ):
133# NCursesUI.Window.__init__( self, x, y, width, height )
134# version = bb.__version__
135# title = "BitBake %s" % version
136# credit = "(C) 2003-2007 Team BitBake"
137# #self.win.hline( 2, 1, curses.ACS_HLINE, width-2 )
138# self.win.border()
139# self.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD )
140# self.setText( 1, 2, credit.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD )
141
142 #-------------------------------------------------------------------------#
143 class ThreadActivityWindow( DecoratedWindow ):
144 #-------------------------------------------------------------------------#
145 """Thread Activity Window"""
146 def __init__( self, x, y, width, height ):
147 NCursesUI.DecoratedWindow.__init__( self, "Thread Activity", x, y, width, height )
148
149 def setStatus( self, thread, text ):
150 line = "%02d: %s" % ( thread, text )
151 width = self.dimensions[WIDTH]
152 if ( len(line) > width ):
153 line = line[:width-3] + "..."
154 else:
155 line = line.ljust( width )
156 self.setText( 0, thread, line )
157
158 #-------------------------------------------------------------------------#
159 class MainWindow( DecoratedWindow ):
160 #-------------------------------------------------------------------------#
161 """Main Window"""
162 def __init__( self, x, y, width, height ):
163 self.StatusPosition = width - MAXSTATUSLENGTH
164 NCursesUI.DecoratedWindow.__init__( self, None, x, y, width, height )
165 curses.nl()
166
167 def setTitle( self, title ):
168 title = "BitBake %s" % bb.__version__
169 self.decoration.setText( 2, 1, title, curses.A_BOLD )
170 self.decoration.setText( self.StatusPosition - 8, 1, "Status:", curses.A_BOLD )
171
172 def setStatus(self, status):
173 while len(status) < MAXSTATUSLENGTH:
174 status = status + " "
175 self.decoration.setText( self.StatusPosition, 1, status, curses.A_BOLD )
176
177
178 #-------------------------------------------------------------------------#
179 class ShellOutputWindow( DecoratedWindow ):
180 #-------------------------------------------------------------------------#
181 """Interactive Command Line Output"""
182 def __init__( self, x, y, width, height ):
183 NCursesUI.DecoratedWindow.__init__( self, "Command Line Window", x, y, width, height )
184
185 #-------------------------------------------------------------------------#
186 class ShellInputWindow( Window ):
187 #-------------------------------------------------------------------------#
188 """Interactive Command Line Input"""
189 def __init__( self, x, y, width, height ):
190 NCursesUI.Window.__init__( self, x, y, width, height )
191
192# put that to the top again from curses.textpad import Textbox
193# self.textbox = Textbox( self.win )
194# t = threading.Thread()
195# t.run = self.textbox.edit
196# t.start()
197
198 #-------------------------------------------------------------------------#
199 def main(self, stdscr, server, eventHandler, params):
200 #-------------------------------------------------------------------------#
201 height, width = stdscr.getmaxyx()
202
203 # for now split it like that:
204 # MAIN_y + THREAD_y = 2/3 screen at the top
205 # MAIN_x = 2/3 left, THREAD_y = 1/3 right
206 # CLI_y = 1/3 of screen at the bottom
207 # CLI_x = full
208
209 main_left = 0
210 main_top = 0
211 main_height = ( height // 3 * 2 )
212 main_width = ( width // 3 ) * 2
213 clo_left = main_left
214 clo_top = main_top + main_height
215 clo_height = height - main_height - main_top - 1
216 clo_width = width
217 cli_left = main_left
218 cli_top = clo_top + clo_height
219 cli_height = 1
220 cli_width = width
221 thread_left = main_left + main_width
222 thread_top = main_top
223 thread_height = main_height
224 thread_width = width - main_width
225
226 #tw = self.TitleWindow( 0, 0, width, main_top )
227 mw = self.MainWindow( main_left, main_top, main_width, main_height )
228 taw = self.ThreadActivityWindow( thread_left, thread_top, thread_width, thread_height )
229 clo = self.ShellOutputWindow( clo_left, clo_top, clo_width, clo_height )
230 cli = self.ShellInputWindow( cli_left, cli_top, cli_width, cli_height )
231 cli.setText( 0, 0, "BB>" )
232
233 mw.setStatus("Idle")
234
235 helper = uihelper.BBUIHelper()
236 shutdown = 0
237
238 try:
239 params.updateFromServer(server)
240 cmdline = params.parseActions()
241 if not cmdline:
242 print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
243 return 1
244 if 'msg' in cmdline and cmdline['msg']:
245 logger.error(cmdline['msg'])
246 return 1
247 cmdline = cmdline['action']
248 ret, error = server.runCommand(cmdline)
249 if error:
250 print("Error running command '%s': %s" % (cmdline, error))
251 return
252 elif ret != True:
253 print("Couldn't get default commandlind! %s" % ret)
254 return
255 except xmlrpclib.Fault as x:
256 print("XMLRPC Fault getting commandline:\n %s" % x)
257 return
258
259 exitflag = False
260 while not exitflag:
261 try:
262 event = eventHandler.waitEvent(0.25)
263 if not event:
264 continue
265
266 helper.eventHandler(event)
267 if isinstance(event, bb.build.TaskBase):
268 mw.appendText("NOTE: %s\n" % event._message)
269 if isinstance(event, logging.LogRecord):
270 mw.appendText(logging.getLevelName(event.levelno) + ': ' + event.getMessage() + '\n')
271
272 if isinstance(event, bb.event.CacheLoadStarted):
273 self.parse_total = event.total
274 if isinstance(event, bb.event.CacheLoadProgress):
275 x = event.current
276 y = self.parse_total
277 mw.setStatus("Loading Cache: %s [%2d %%]" % ( next(parsespin), x*100/y ) )
278 if isinstance(event, bb.event.CacheLoadCompleted):
279 mw.setStatus("Idle")
280 mw.appendText("Loaded %d entries from dependency cache.\n"
281 % ( event.num_entries))
282
283 if isinstance(event, bb.event.ParseStarted):
284 self.parse_total = event.total
285 if isinstance(event, bb.event.ParseProgress):
286 x = event.current
287 y = self.parse_total
288 mw.setStatus("Parsing Recipes: %s [%2d %%]" % ( next(parsespin), x*100/y ) )
289 if isinstance(event, bb.event.ParseCompleted):
290 mw.setStatus("Idle")
291 mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked.\n"
292 % ( event.cached, event.parsed, event.skipped, event.masked ))
293
294# if isinstance(event, bb.build.TaskFailed):
295# if event.logfile:
296# if data.getVar("BBINCLUDELOGS", d):
297# bb.error("log data follows (%s)" % logfile)
298# number_of_lines = data.getVar("BBINCLUDELOGS_LINES", d)
299# if number_of_lines:
300# subprocess.call('tail -n%s %s' % (number_of_lines, logfile), shell=True)
301# else:
302# f = open(logfile, "r")
303# while True:
304# l = f.readline()
305# if l == '':
306# break
307# l = l.rstrip()
308# print '| %s' % l
309# f.close()
310# else:
311# bb.error("see log in %s" % logfile)
312
313 if isinstance(event, bb.command.CommandCompleted):
314 # stop so the user can see the result of the build, but
315 # also allow them to now exit with a single ^C
316 shutdown = 2
317 if isinstance(event, bb.command.CommandFailed):
318 mw.appendText("Command execution failed: %s" % event.error)
319 time.sleep(2)
320 exitflag = True
321 if isinstance(event, bb.command.CommandExit):
322 exitflag = True
323 if isinstance(event, bb.cooker.CookerExit):
324 exitflag = True
325
326 if isinstance(event, bb.event.LogExecTTY):
327 mw.appendText('WARN: ' + event.msg + '\n')
328 if helper.needUpdate:
329 activetasks, failedtasks = helper.getTasks()
330 taw.erase()
331 taw.setText(0, 0, "")
332 if activetasks:
333 taw.appendText("Active Tasks:\n")
334 for task in activetasks.itervalues():
335 taw.appendText(task["title"] + '\n')
336 if failedtasks:
337 taw.appendText("Failed Tasks:\n")
338 for task in failedtasks:
339 taw.appendText(task["title"] + '\n')
340
341 curses.doupdate()
342 except EnvironmentError as ioerror:
343 # ignore interrupted io
344 if ioerror.args[0] == 4:
345 pass
346
347 except KeyboardInterrupt:
348 if shutdown == 2:
349 mw.appendText("Third Keyboard Interrupt, exit.\n")
350 exitflag = True
351 if shutdown == 1:
352 mw.appendText("Second Keyboard Interrupt, stopping...\n")
353 _, error = server.runCommand(["stateForceShutdown"])
354 if error:
355 print("Unable to cleanly stop: %s" % error)
356 if shutdown == 0:
357 mw.appendText("Keyboard Interrupt, closing down...\n")
358 _, error = server.runCommand(["stateShutdown"])
359 if error:
360 print("Unable to cleanly shutdown: %s" % error)
361 shutdown = shutdown + 1
362 pass
363
364def main(server, eventHandler):
365 if not os.isatty(sys.stdout.fileno()):
366 print("FATAL: Unable to run 'ncurses' UI without a TTY.")
367 return
368 ui = NCursesUI()
369 try:
370 curses.wrapper(ui.main, server, eventHandler)
371 except:
372 import traceback
373 traceback.print_exc()
diff --git a/bitbake/lib/bb/ui/puccho.py b/bitbake/lib/bb/ui/puccho.py
new file mode 100644
index 0000000000..3ce4590c16
--- /dev/null
+++ b/bitbake/lib/bb/ui/puccho.py
@@ -0,0 +1,425 @@
1#
2# BitBake Graphical GTK User Interface
3#
4# Copyright (C) 2008 Intel Corporation
5#
6# Authored by Rob Bradford <rob@linux.intel.com>
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21import gtk
22import gobject
23import gtk.glade
24import threading
25import urllib2
26import os
27import contextlib
28
29from bb.ui.crumbs.buildmanager import BuildManager, BuildConfiguration
30from bb.ui.crumbs.buildmanager import BuildManagerTreeView
31
32from bb.ui.crumbs.runningbuild import RunningBuild, RunningBuildTreeView
33
34# The metadata loader is used by the BuildSetupDialog to download the
35# available options to populate the dialog
36class MetaDataLoader(gobject.GObject):
37 """ This class provides the mechanism for loading the metadata (the
38 fetching and parsing) from a given URL. The metadata encompasses details
39 on what machines are available. The distribution and images available for
40 the machine and the the uris to use for building the given machine."""
41 __gsignals__ = {
42 'success' : (gobject.SIGNAL_RUN_LAST,
43 gobject.TYPE_NONE,
44 ()),
45 'error' : (gobject.SIGNAL_RUN_LAST,
46 gobject.TYPE_NONE,
47 (gobject.TYPE_STRING,))
48 }
49
50 # We use these little helper functions to ensure that we take the gdk lock
51 # when emitting the signal. These functions are called as idles (so that
52 # they happen in the gtk / main thread's main loop.
53 def emit_error_signal (self, remark):
54 gtk.gdk.threads_enter()
55 self.emit ("error", remark)
56 gtk.gdk.threads_leave()
57
58 def emit_success_signal (self):
59 gtk.gdk.threads_enter()
60 self.emit ("success")
61 gtk.gdk.threads_leave()
62
63 def __init__ (self):
64 gobject.GObject.__init__ (self)
65
66 class LoaderThread(threading.Thread):
67 """ This class provides an asynchronous loader for the metadata (by
68 using threads and signals). This is useful since the metadata may be
69 at a remote URL."""
70 class LoaderImportException (Exception):
71 pass
72
73 def __init__(self, loader, url):
74 threading.Thread.__init__ (self)
75 self.url = url
76 self.loader = loader
77
78 def run (self):
79 result = {}
80 try:
81 with contextlib.closing (urllib2.urlopen (self.url)) as f:
82 # Parse the metadata format. The format is....
83 # <machine>;<default distro>|<distro>...;<default image>|<image>...;<type##url>|...
84 for line in f:
85 components = line.split(";")
86 if (len (components) < 4):
87 raise MetaDataLoader.LoaderThread.LoaderImportException
88 machine = components[0]
89 distros = components[1].split("|")
90 images = components[2].split("|")
91 urls = components[3].split("|")
92
93 result[machine] = (distros, images, urls)
94
95 # Create an object representing this *potential*
96 # configuration. It can become concrete if the machine, distro
97 # and image are all chosen in the UI
98 configuration = BuildConfiguration()
99 configuration.metadata_url = self.url
100 configuration.machine_options = result
101 self.loader.configuration = configuration
102
103 # Emit that we've actually got a configuration
104 gobject.idle_add (MetaDataLoader.emit_success_signal,
105 self.loader)
106
107 except MetaDataLoader.LoaderThread.LoaderImportException as e:
108 gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader,
109 "Repository metadata corrupt")
110 except Exception as e:
111 gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader,
112 "Unable to download repository metadata")
113 print(e)
114
115 def try_fetch_from_url (self, url):
116 # Try and download the metadata. Firing a signal if successful
117 thread = MetaDataLoader.LoaderThread(self, url)
118 thread.start()
119
120class BuildSetupDialog (gtk.Dialog):
121 RESPONSE_BUILD = 1
122
123 # A little helper method that just sets the states on the widgets based on
124 # whether we've got good metadata or not.
125 def set_configurable (self, configurable):
126 if (self.configurable == configurable):
127 return
128
129 self.configurable = configurable
130 for widget in self.conf_widgets:
131 widget.set_sensitive (configurable)
132
133 if not configurable:
134 self.machine_combo.set_active (-1)
135 self.distribution_combo.set_active (-1)
136 self.image_combo.set_active (-1)
137
138 # GTK widget callbacks
139 def refresh_button_clicked (self, button):
140 # Refresh button clicked.
141
142 url = self.location_entry.get_chars (0, -1)
143 self.loader.try_fetch_from_url(url)
144
145 def repository_entry_editable_changed (self, entry):
146 if (len (entry.get_chars (0, -1)) > 0):
147 self.refresh_button.set_sensitive (True)
148 else:
149 self.refresh_button.set_sensitive (False)
150 self.clear_status_message()
151
152 # If we were previously configurable we are no longer since the
153 # location entry has been changed
154 self.set_configurable (False)
155
156 def machine_combo_changed (self, combobox):
157 active_iter = combobox.get_active_iter()
158
159 if not active_iter:
160 return
161
162 model = combobox.get_model()
163
164 if model:
165 chosen_machine = model.get (active_iter, 0)[0]
166
167 (distros_model, images_model) = \
168 self.loader.configuration.get_distro_and_images_models (chosen_machine)
169
170 self.distribution_combo.set_model (distros_model)
171 self.image_combo.set_model (images_model)
172
173 # Callbacks from the loader
174 def loader_success_cb (self, loader):
175 self.status_image.set_from_icon_name ("info",
176 gtk.ICON_SIZE_BUTTON)
177 self.status_image.show()
178 self.status_label.set_label ("Repository metadata successfully downloaded")
179
180 # Set the models on the combo boxes based on the models generated from
181 # the configuration that the loader has created
182
183 # We just need to set the machine here, that then determines the
184 # distro and image options. Cunning huh? :-)
185
186 self.configuration = self.loader.configuration
187 model = self.configuration.get_machines_model ()
188 self.machine_combo.set_model (model)
189
190 self.set_configurable (True)
191
192 def loader_error_cb (self, loader, message):
193 self.status_image.set_from_icon_name ("error",
194 gtk.ICON_SIZE_BUTTON)
195 self.status_image.show()
196 self.status_label.set_text ("Error downloading repository metadata")
197 for widget in self.conf_widgets:
198 widget.set_sensitive (False)
199
200 def clear_status_message (self):
201 self.status_image.hide()
202 self.status_label.set_label (
203 """<i>Enter the repository location and press _Refresh</i>""")
204
205 def __init__ (self):
206 gtk.Dialog.__init__ (self)
207
208 # Cancel
209 self.add_button (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
210
211 # Build
212 button = gtk.Button ("_Build", None, True)
213 image = gtk.Image ()
214 image.set_from_stock (gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
215 button.set_image (image)
216 self.add_action_widget (button, BuildSetupDialog.RESPONSE_BUILD)
217 button.show_all ()
218
219 # Pull in *just* the table from the Glade XML data.
220 gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade",
221 root = "build_table")
222 table = gxml.get_widget ("build_table")
223 self.vbox.pack_start (table, True, False, 0)
224
225 # Grab all the widgets that we need to turn on/off when we refresh...
226 self.conf_widgets = []
227 self.conf_widgets += [gxml.get_widget ("machine_label")]
228 self.conf_widgets += [gxml.get_widget ("distribution_label")]
229 self.conf_widgets += [gxml.get_widget ("image_label")]
230 self.conf_widgets += [gxml.get_widget ("machine_combo")]
231 self.conf_widgets += [gxml.get_widget ("distribution_combo")]
232 self.conf_widgets += [gxml.get_widget ("image_combo")]
233
234 # Grab the status widgets
235 self.status_image = gxml.get_widget ("status_image")
236 self.status_label = gxml.get_widget ("status_label")
237
238 # Grab the refresh button and connect to the clicked signal
239 self.refresh_button = gxml.get_widget ("refresh_button")
240 self.refresh_button.connect ("clicked", self.refresh_button_clicked)
241
242 # Grab the location entry and connect to editable::changed
243 self.location_entry = gxml.get_widget ("location_entry")
244 self.location_entry.connect ("changed",
245 self.repository_entry_editable_changed)
246
247 # Grab the machine combo and hook onto the changed signal. This then
248 # allows us to populate the distro and image combos
249 self.machine_combo = gxml.get_widget ("machine_combo")
250 self.machine_combo.connect ("changed", self.machine_combo_changed)
251
252 # Setup the combo
253 cell = gtk.CellRendererText()
254 self.machine_combo.pack_start(cell, True)
255 self.machine_combo.add_attribute(cell, 'text', 0)
256
257 # Grab the distro and image combos. We need these to populate with
258 # models once the machine is chosen
259 self.distribution_combo = gxml.get_widget ("distribution_combo")
260 cell = gtk.CellRendererText()
261 self.distribution_combo.pack_start(cell, True)
262 self.distribution_combo.add_attribute(cell, 'text', 0)
263
264 self.image_combo = gxml.get_widget ("image_combo")
265 cell = gtk.CellRendererText()
266 self.image_combo.pack_start(cell, True)
267 self.image_combo.add_attribute(cell, 'text', 0)
268
269 # Put the default descriptive text in the status box
270 self.clear_status_message()
271
272 # Mark as non-configurable, this is just greys out the widgets the
273 # user can't yet use
274 self.configurable = False
275 self.set_configurable(False)
276
277 # Show the table
278 table.show_all ()
279
280 # The loader and some signals connected to it to update the status
281 # area
282 self.loader = MetaDataLoader()
283 self.loader.connect ("success", self.loader_success_cb)
284 self.loader.connect ("error", self.loader_error_cb)
285
286 def update_configuration (self):
287 """ A poorly named function but it updates the internal configuration
288 from the widgets. This can make that configuration concrete and can
289 thus be used for building """
290 # Extract the chosen machine from the combo
291 model = self.machine_combo.get_model()
292 active_iter = self.machine_combo.get_active_iter()
293 if (active_iter):
294 self.configuration.machine = model.get(active_iter, 0)[0]
295
296 # Extract the chosen distro from the combo
297 model = self.distribution_combo.get_model()
298 active_iter = self.distribution_combo.get_active_iter()
299 if (active_iter):
300 self.configuration.distro = model.get(active_iter, 0)[0]
301
302 # Extract the chosen image from the combo
303 model = self.image_combo.get_model()
304 active_iter = self.image_combo.get_active_iter()
305 if (active_iter):
306 self.configuration.image = model.get(active_iter, 0)[0]
307
308# This function operates to pull events out from the event queue and then push
309# them into the RunningBuild (which then drives the RunningBuild which then
310# pushes through and updates the progress tree view.)
311#
312# TODO: Should be a method on the RunningBuild class
313def event_handle_timeout (eventHandler, build):
314 # Consume as many messages as we can ...
315 event = eventHandler.getEvent()
316 while event:
317 build.handle_event (event)
318 event = eventHandler.getEvent()
319 return True
320
321class MainWindow (gtk.Window):
322
323 # Callback that gets fired when the user hits a button in the
324 # BuildSetupDialog.
325 def build_dialog_box_response_cb (self, dialog, response_id):
326 conf = None
327 if (response_id == BuildSetupDialog.RESPONSE_BUILD):
328 dialog.update_configuration()
329 print(dialog.configuration.machine, dialog.configuration.distro, \
330 dialog.configuration.image)
331 conf = dialog.configuration
332
333 dialog.destroy()
334
335 if conf:
336 self.manager.do_build (conf)
337
338 def build_button_clicked_cb (self, button):
339 dialog = BuildSetupDialog ()
340
341 # For some unknown reason Dialog.run causes nice little deadlocks ... :-(
342 dialog.connect ("response", self.build_dialog_box_response_cb)
343 dialog.show()
344
345 def __init__ (self):
346 gtk.Window.__init__ (self)
347
348 # Pull in *just* the main vbox from the Glade XML data and then pack
349 # that inside the window
350 gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade",
351 root = "main_window_vbox")
352 vbox = gxml.get_widget ("main_window_vbox")
353 self.add (vbox)
354
355 # Create the tree views for the build manager view and the progress view
356 self.build_manager_view = BuildManagerTreeView()
357 self.running_build_view = RunningBuildTreeView()
358
359 # Grab the scrolled windows that we put the tree views into
360 self.results_scrolledwindow = gxml.get_widget ("results_scrolledwindow")
361 self.progress_scrolledwindow = gxml.get_widget ("progress_scrolledwindow")
362
363 # Put the tree views inside ...
364 self.results_scrolledwindow.add (self.build_manager_view)
365 self.progress_scrolledwindow.add (self.running_build_view)
366
367 # Hook up the build button...
368 self.build_button = gxml.get_widget ("main_toolbutton_build")
369 self.build_button.connect ("clicked", self.build_button_clicked_cb)
370
371# I'm not very happy about the current ownership of the RunningBuild. I have
372# my suspicions that this object should be held by the BuildManager since we
373# care about the signals in the manager
374
375def running_build_succeeded_cb (running_build, manager):
376 # Notify the manager that a build has succeeded. This is necessary as part
377 # of the 'hack' that we use for making the row in the model / view
378 # representing the ongoing build change into a row representing the
379 # completed build. Since we know only one build can be running a time then
380 # we can handle this.
381
382 # FIXME: Refactor all this so that the RunningBuild is owned by the
383 # BuildManager. It can then hook onto the signals directly and drive
384 # interesting things it cares about.
385 manager.notify_build_succeeded ()
386 print("build succeeded")
387
388def running_build_failed_cb (running_build, manager):
389 # As above
390 print("build failed")
391 manager.notify_build_failed ()
392
393def main (server, eventHandler):
394 # Initialise threading...
395 gobject.threads_init()
396 gtk.gdk.threads_init()
397
398 main_window = MainWindow ()
399 main_window.show_all ()
400
401 # Set up the build manager stuff in general
402 builds_dir = os.path.join (os.getcwd(), "results")
403 manager = BuildManager (server, builds_dir)
404 main_window.build_manager_view.set_model (manager.model)
405
406 # Do the running build setup
407 running_build = RunningBuild ()
408 main_window.running_build_view.set_model (running_build.model)
409 running_build.connect ("build-succeeded", running_build_succeeded_cb,
410 manager)
411 running_build.connect ("build-failed", running_build_failed_cb, manager)
412
413 # We need to save the manager into the MainWindow so that the toolbar
414 # button can use it.
415 # FIXME: Refactor ?
416 main_window.manager = manager
417
418 # Use a timeout function for probing the event queue to find out if we
419 # have a message waiting for us.
420 gobject.timeout_add (200,
421 event_handle_timeout,
422 eventHandler,
423 running_build)
424
425 gtk.main()
diff --git a/bitbake/lib/bb/ui/uievent.py b/bitbake/lib/bb/ui/uievent.py
new file mode 100644
index 0000000000..2133b44477
--- /dev/null
+++ b/bitbake/lib/bb/ui/uievent.py
@@ -0,0 +1,133 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer
5# Copyright (C) 2006 - 2007 Richard Purdie
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
21"""
22Use this class to fork off a thread to recieve event callbacks from the bitbake
23server and queue them for the UI to process. This process must be used to avoid
24client/server deadlocks.
25"""
26
27import socket, threading, pickle
28from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
29
30class BBUIEventQueue:
31 def __init__(self, BBServer, clientinfo=("localhost, 0"), featureset=[]):
32
33 self.eventQueue = []
34 self.eventQueueLock = threading.Lock()
35 self.eventQueueNotify = threading.Event()
36
37 self.BBServer = BBServer
38 self.clientinfo = clientinfo
39
40 server = UIXMLRPCServer(self.clientinfo)
41 self.host, self.port = server.socket.getsockname()
42
43 server.register_function( self.system_quit, "event.quit" )
44 server.register_function( self.send_event, "event.sendpickle" )
45 server.socket.settimeout(1)
46
47 self.EventHandle = self.BBServer.registerEventHandler(self.host, self.port, featureset)
48
49 if (self.EventHandle == None):
50 bb.fatal("Could not register UI event handler")
51
52 self.server = server
53
54 self.t = threading.Thread()
55 self.t.setDaemon(True)
56 self.t.run = self.startCallbackHandler
57 self.t.start()
58
59 def getEvent(self):
60
61 self.eventQueueLock.acquire()
62
63 if len(self.eventQueue) == 0:
64 self.eventQueueLock.release()
65 return None
66
67 item = self.eventQueue.pop(0)
68
69 if len(self.eventQueue) == 0:
70 self.eventQueueNotify.clear()
71
72 self.eventQueueLock.release()
73 return item
74
75 def waitEvent(self, delay):
76 self.eventQueueNotify.wait(delay)
77 return self.getEvent()
78
79 def queue_event(self, event):
80 self.eventQueueLock.acquire()
81 self.eventQueue.append(event)
82 self.eventQueueNotify.set()
83 self.eventQueueLock.release()
84
85 def send_event(self, event):
86 self.queue_event(pickle.loads(event))
87
88 def startCallbackHandler(self):
89
90 self.server.timeout = 1
91 while not self.server.quit:
92 self.server.handle_request()
93 self.server.server_close()
94
95 def system_quit( self ):
96 """
97 Shut down the callback thread
98 """
99 try:
100 self.BBServer.unregisterEventHandler(self.EventHandle)
101 except:
102 pass
103 self.server.quit = True
104
105class UIXMLRPCServer (SimpleXMLRPCServer):
106
107 def __init__( self, interface ):
108 self.quit = False
109 SimpleXMLRPCServer.__init__( self,
110 interface,
111 requestHandler=SimpleXMLRPCRequestHandler,
112 logRequests=False, allow_none=True)
113
114 def get_request(self):
115 while not self.quit:
116 try:
117 sock, addr = self.socket.accept()
118 sock.settimeout(1)
119 return (sock, addr)
120 except socket.timeout:
121 pass
122 return (None, None)
123
124 def close_request(self, request):
125 if request is None:
126 return
127 SimpleXMLRPCServer.close_request(self, request)
128
129 def process_request(self, request, client_address):
130 if request is None:
131 return
132 SimpleXMLRPCServer.process_request(self, request, client_address)
133
diff --git a/bitbake/lib/bb/ui/uihelper.py b/bitbake/lib/bb/ui/uihelper.py
new file mode 100644
index 0000000000..a703387fb8
--- /dev/null
+++ b/bitbake/lib/bb/ui/uihelper.py
@@ -0,0 +1,100 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3#
4# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer
5# Copyright (C) 2006 - 2007 Richard Purdie
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20import bb.build
21
22class BBUIHelper:
23 def __init__(self):
24 self.needUpdate = False
25 self.running_tasks = {}
26 # Running PIDs preserves the order tasks were executed in
27 self.running_pids = []
28 self.failed_tasks = []
29 self.tasknumber_current = 0
30 self.tasknumber_total = 0
31
32 def eventHandler(self, event):
33 if isinstance(event, bb.build.TaskStarted):
34 self.running_tasks[event.pid] = { 'title' : "%s %s" % (event._package, event._task) }
35 self.running_pids.append(event.pid)
36 self.needUpdate = True
37 if isinstance(event, bb.build.TaskSucceeded):
38 del self.running_tasks[event.pid]
39 self.running_pids.remove(event.pid)
40 self.needUpdate = True
41 if isinstance(event, bb.build.TaskFailedSilent):
42 del self.running_tasks[event.pid]
43 self.running_pids.remove(event.pid)
44 # Don't add to the failed tasks list since this is e.g. a setscene task failure
45 self.needUpdate = True
46 if isinstance(event, bb.build.TaskFailed):
47 del self.running_tasks[event.pid]
48 self.running_pids.remove(event.pid)
49 self.failed_tasks.append( { 'title' : "%s %s" % (event._package, event._task)})
50 self.needUpdate = True
51 if isinstance(event, bb.runqueue.runQueueTaskStarted) or isinstance(event, bb.runqueue.sceneQueueTaskStarted):
52 self.tasknumber_current = event.stats.completed + event.stats.active + event.stats.failed + 1
53 self.tasknumber_total = event.stats.total
54 self.needUpdate = True
55
56 def getTasks(self):
57 self.needUpdate = False
58 return (self.running_tasks, self.failed_tasks)
59
60 def findServerDetails(self):
61 import sys
62 import optparse
63 from bb.server.xmlrpc import BitbakeServerInfo, BitBakeServerConnection
64 host = ""
65 port = 0
66 bind = ""
67 parser = optparse.OptionParser(
68 usage = """%prog -H host -P port -B bindaddr""")
69
70 parser.add_option("-H", "--host", help = "Bitbake server's IP address",
71 action = "store", dest = "host", default = None)
72
73 parser.add_option("-P", "--port", help = "Bitbake server's Port number",
74 action = "store", dest = "port", default = None)
75
76 parser.add_option("-B", "--bind", help = "Hob2 local bind address",
77 action = "store", dest = "bind", default = None)
78
79 options, args = parser.parse_args(sys.argv)
80 for key, val in options.__dict__.items():
81 if key == 'host' and val:
82 host = val
83 elif key == 'port' and val:
84 port = int(val)
85 elif key == 'bind' and val:
86 bind = val
87
88 if not host or not port or not bind:
89 parser.print_usage()
90 sys.exit(1)
91
92 serverinfo = BitbakeServerInfo(host, port)
93 clientinfo = (bind, 0)
94 connection = BitBakeServerConnection(serverinfo, clientinfo)
95
96 server = connection.connection
97 eventHandler = connection.events
98
99 return server, eventHandler, host, bind
100
diff --git a/bitbake/lib/bb/utils.py b/bitbake/lib/bb/utils.py
new file mode 100644
index 0000000000..f9ee4f1c1d
--- /dev/null
+++ b/bitbake/lib/bb/utils.py
@@ -0,0 +1,869 @@
1# ex:ts=4:sw=4:sts=4:et
2# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3"""
4BitBake Utility Functions
5"""
6
7# Copyright (C) 2004 Michael Lauer
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22import re, fcntl, os, string, stat, shutil, time
23import sys
24import errno
25import logging
26import bb
27import bb.msg
28import multiprocessing
29import fcntl
30import subprocess
31import glob
32import traceback
33import errno
34from commands import getstatusoutput
35from contextlib import contextmanager
36
37logger = logging.getLogger("BitBake.Util")
38
39def clean_context():
40 return {
41 "os": os,
42 "bb": bb,
43 "time": time,
44 }
45
46def get_context():
47 return _context
48
49
50def set_context(ctx):
51 _context = ctx
52
53# Context used in better_exec, eval
54_context = clean_context()
55
56def explode_version(s):
57 r = []
58 alpha_regexp = re.compile('^([a-zA-Z]+)(.*)$')
59 numeric_regexp = re.compile('^(\d+)(.*)$')
60 while (s != ''):
61 if s[0] in string.digits:
62 m = numeric_regexp.match(s)
63 r.append((0, int(m.group(1))))
64 s = m.group(2)
65 continue
66 if s[0] in string.letters:
67 m = alpha_regexp.match(s)
68 r.append((1, m.group(1)))
69 s = m.group(2)
70 continue
71 if s[0] == '~':
72 r.append((-1, s[0]))
73 else:
74 r.append((2, s[0]))
75 s = s[1:]
76 return r
77
78def split_version(s):
79 """Split a version string into its constituent parts (PE, PV, PR)"""
80 s = s.strip(" <>=")
81 e = 0
82 if s.count(':'):
83 e = int(s.split(":")[0])
84 s = s.split(":")[1]
85 r = ""
86 if s.count('-'):
87 r = s.rsplit("-", 1)[1]
88 s = s.rsplit("-", 1)[0]
89 v = s
90 return (e, v, r)
91
92def vercmp_part(a, b):
93 va = explode_version(a)
94 vb = explode_version(b)
95 while True:
96 if va == []:
97 (oa, ca) = (0, None)
98 else:
99 (oa, ca) = va.pop(0)
100 if vb == []:
101 (ob, cb) = (0, None)
102 else:
103 (ob, cb) = vb.pop(0)
104 if (oa, ca) == (0, None) and (ob, cb) == (0, None):
105 return 0
106 if oa < ob:
107 return -1
108 elif oa > ob:
109 return 1
110 elif ca < cb:
111 return -1
112 elif ca > cb:
113 return 1
114
115def vercmp(ta, tb):
116 (ea, va, ra) = ta
117 (eb, vb, rb) = tb
118
119 r = int(ea or 0) - int(eb or 0)
120 if (r == 0):
121 r = vercmp_part(va, vb)
122 if (r == 0):
123 r = vercmp_part(ra, rb)
124 return r
125
126def vercmp_string(a, b):
127 ta = split_version(a)
128 tb = split_version(b)
129 return vercmp(ta, tb)
130
131def explode_deps(s):
132 """
133 Take an RDEPENDS style string of format:
134 "DEPEND1 (optional version) DEPEND2 (optional version) ..."
135 and return a list of dependencies.
136 Version information is ignored.
137 """
138 r = []
139 l = s.split()
140 flag = False
141 for i in l:
142 if i[0] == '(':
143 flag = True
144 #j = []
145 if not flag:
146 r.append(i)
147 #else:
148 # j.append(i)
149 if flag and i.endswith(')'):
150 flag = False
151 # Ignore version
152 #r[-1] += ' ' + ' '.join(j)
153 return r
154
155def explode_dep_versions2(s):
156 """
157 Take an RDEPENDS style string of format:
158 "DEPEND1 (optional version) DEPEND2 (optional version) ..."
159 and return a dictionary of dependencies and versions.
160 """
161 r = {}
162 l = s.replace(",", "").split()
163 lastdep = None
164 lastcmp = ""
165 lastver = ""
166 incmp = False
167 inversion = False
168 for i in l:
169 if i[0] == '(':
170 incmp = True
171 i = i[1:].strip()
172 if not i:
173 continue
174
175 if incmp:
176 incmp = False
177 inversion = True
178 # This list is based on behavior and supported comparisons from deb, opkg and rpm.
179 #
180 # Even though =<, <<, ==, !=, =>, and >> may not be supported,
181 # we list each possibly valid item.
182 # The build system is responsible for validation of what it supports.
183 if i.startswith(('<=', '=<', '<<', '==', '!=', '>=', '=>', '>>')):
184 lastcmp = i[0:2]
185 i = i[2:]
186 elif i.startswith(('<', '>', '=')):
187 lastcmp = i[0:1]
188 i = i[1:]
189 else:
190 # This is an unsupported case!
191 lastcmp = (i or "")
192 i = ""
193 i.strip()
194 if not i:
195 continue
196
197 if inversion:
198 if i.endswith(')'):
199 i = i[:-1] or ""
200 inversion = False
201 if lastver and i:
202 lastver += " "
203 if i:
204 lastver += i
205 if lastdep not in r:
206 r[lastdep] = []
207 r[lastdep].append(lastcmp + " " + lastver)
208 continue
209
210 #if not inversion:
211 lastdep = i
212 lastver = ""
213 lastcmp = ""
214 if not (i in r and r[i]):
215 r[lastdep] = []
216
217 return r
218
219def explode_dep_versions(s):
220 r = explode_dep_versions2(s)
221 for d in r:
222 if not r[d]:
223 r[d] = None
224 continue
225 if len(r[d]) > 1:
226 bb.warn("explode_dep_versions(): Item %s appeared in dependency string '%s' multiple times with different values. explode_dep_versions cannot cope with this." % (d, s))
227 r[d] = r[d][0]
228 return r
229
230def join_deps(deps, commasep=True):
231 """
232 Take the result from explode_dep_versions and generate a dependency string
233 """
234 result = []
235 for dep in deps:
236 if deps[dep]:
237 if isinstance(deps[dep], list):
238 for v in deps[dep]:
239 result.append(dep + " (" + v + ")")
240 else:
241 result.append(dep + " (" + deps[dep] + ")")
242 else:
243 result.append(dep)
244 if commasep:
245 return ", ".join(result)
246 else:
247 return " ".join(result)
248
249def _print_trace(body, line):
250 """
251 Print the Environment of a Text Body
252 """
253 error = []
254 # print the environment of the method
255 min_line = max(1, line-4)
256 max_line = min(line + 4, len(body))
257 for i in range(min_line, max_line + 1):
258 if line == i:
259 error.append(' *** %.4d:%s' % (i, body[i-1].rstrip()))
260 else:
261 error.append(' %.4d:%s' % (i, body[i-1].rstrip()))
262 return error
263
264def better_compile(text, file, realfile, mode = "exec"):
265 """
266 A better compile method. This method
267 will print the offending lines.
268 """
269 try:
270 return compile(text, file, mode)
271 except Exception as e:
272 error = []
273 # split the text into lines again
274 body = text.split('\n')
275 error.append("Error in compiling python function in %s:\n" % realfile)
276 if e.lineno:
277 error.append("The code lines resulting in this error were:")
278 error.extend(_print_trace(body, e.lineno))
279 else:
280 error.append("The function causing this error was:")
281 for line in body:
282 error.append(line)
283 error.append("%s: %s" % (e.__class__.__name__, str(e)))
284
285 logger.error("\n".join(error))
286
287 e = bb.BBHandledException(e)
288 raise e
289
290def _print_exception(t, value, tb, realfile, text, context):
291 error = []
292 try:
293 exception = traceback.format_exception_only(t, value)
294 error.append('Error executing a python function in %s:\n' % realfile)
295
296 # Strip 'us' from the stack (better_exec call)
297 tb = tb.tb_next
298
299 textarray = text.split('\n')
300
301 linefailed = tb.tb_lineno
302
303 tbextract = traceback.extract_tb(tb)
304 tbformat = traceback.format_list(tbextract)
305 error.append("The stack trace of python calls that resulted in this exception/failure was:")
306 error.append("File: '%s', lineno: %s, function: %s" % (tbextract[0][0], tbextract[0][1], tbextract[0][2]))
307 error.extend(_print_trace(textarray, linefailed))
308
309 # See if this is a function we constructed and has calls back into other functions in
310 # "text". If so, try and improve the context of the error by diving down the trace
311 level = 0
312 nexttb = tb.tb_next
313 while nexttb is not None and (level+1) < len(tbextract):
314 error.append("File: '%s', lineno: %s, function: %s" % (tbextract[level+1][0], tbextract[level+1][1], tbextract[level+1][2]))
315 if tbextract[level][0] == tbextract[level+1][0] and tbextract[level+1][2] == tbextract[level][0]:
316 # The code was possibly in the string we compiled ourselves
317 error.extend(_print_trace(textarray, tbextract[level+1][1]))
318 elif tbextract[level+1][0].startswith("/"):
319 # The code looks like it might be in a file, try and load it
320 try:
321 with open(tbextract[level+1][0], "r") as f:
322 text = f.readlines()
323 error.extend(_print_trace(text, tbextract[level+1][1]))
324 except:
325 error.append(tbformat[level+1])
326 elif "d" in context and tbextract[level+1][2]:
327 # Try and find the code in the datastore based on the functionname
328 d = context["d"]
329 functionname = tbextract[level+1][2]
330 text = d.getVar(functionname, True)
331 if text:
332 error.extend(_print_trace(text.split('\n'), tbextract[level+1][1]))
333 else:
334 error.append(tbformat[level+1])
335 else:
336 error.append(tbformat[level+1])
337 nexttb = tb.tb_next
338 level = level + 1
339
340 error.append("Exception: %s" % ''.join(exception))
341 finally:
342 logger.error("\n".join(error))
343
344def better_exec(code, context, text = None, realfile = "<code>"):
345 """
346 Similiar to better_compile, better_exec will
347 print the lines that are responsible for the
348 error.
349 """
350 import bb.parse
351 if not text:
352 text = code
353 if not hasattr(code, "co_filename"):
354 code = better_compile(code, realfile, realfile)
355 try:
356 exec(code, get_context(), context)
357 except Exception as e:
358 (t, value, tb) = sys.exc_info()
359
360 if t in [bb.parse.SkipPackage, bb.build.FuncFailed]:
361 raise
362 try:
363 _print_exception(t, value, tb, realfile, text, context)
364 except Exception as e:
365 logger.error("Exception handler error: %s" % str(e))
366
367 e = bb.BBHandledException(e)
368 raise e
369
370def simple_exec(code, context):
371 exec(code, get_context(), context)
372
373def better_eval(source, locals):
374 return eval(source, get_context(), locals)
375
376@contextmanager
377def fileslocked(files):
378 """Context manager for locking and unlocking file locks."""
379 locks = []
380 if files:
381 for lockfile in files:
382 locks.append(bb.utils.lockfile(lockfile))
383
384 yield
385
386 for lock in locks:
387 bb.utils.unlockfile(lock)
388
389def lockfile(name, shared=False, retry=True):
390 """
391 Use the file fn as a lock file, return when the lock has been acquired.
392 Returns a variable to pass to unlockfile().
393 """
394 dirname = os.path.dirname(name)
395 mkdirhier(dirname)
396
397 if not os.access(dirname, os.W_OK):
398 logger.error("Unable to acquire lock '%s', directory is not writable",
399 name)
400 sys.exit(1)
401
402 op = fcntl.LOCK_EX
403 if shared:
404 op = fcntl.LOCK_SH
405 if not retry:
406 op = op | fcntl.LOCK_NB
407
408 while True:
409 # If we leave the lockfiles lying around there is no problem
410 # but we should clean up after ourselves. This gives potential
411 # for races though. To work around this, when we acquire the lock
412 # we check the file we locked was still the lock file on disk.
413 # by comparing inode numbers. If they don't match or the lockfile
414 # no longer exists, we start again.
415
416 # This implementation is unfair since the last person to request the
417 # lock is the most likely to win it.
418
419 try:
420 lf = open(name, 'a+')
421 fileno = lf.fileno()
422 fcntl.flock(fileno, op)
423 statinfo = os.fstat(fileno)
424 if os.path.exists(lf.name):
425 statinfo2 = os.stat(lf.name)
426 if statinfo.st_ino == statinfo2.st_ino:
427 return lf
428 lf.close()
429 except Exception:
430 try:
431 lf.close()
432 except Exception:
433 pass
434 pass
435 if not retry:
436 return None
437
438def unlockfile(lf):
439 """
440 Unlock a file locked using lockfile()
441 """
442 try:
443 # If we had a shared lock, we need to promote to exclusive before
444 # removing the lockfile. Attempt this, ignore failures.
445 fcntl.flock(lf.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB)
446 os.unlink(lf.name)
447 except (IOError, OSError):
448 pass
449 fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
450 lf.close()
451
452def md5_file(filename):
453 """
454 Return the hex string representation of the MD5 checksum of filename.
455 """
456 try:
457 import hashlib
458 m = hashlib.md5()
459 except ImportError:
460 import md5
461 m = md5.new()
462
463 with open(filename, "rb") as f:
464 for line in f:
465 m.update(line)
466 return m.hexdigest()
467
468def sha256_file(filename):
469 """
470 Return the hex string representation of the 256-bit SHA checksum of
471 filename. On Python 2.4 this will return None, so callers will need to
472 handle that by either skipping SHA checks, or running a standalone sha256sum
473 binary.
474 """
475 try:
476 import hashlib
477 except ImportError:
478 return None
479
480 s = hashlib.sha256()
481 with open(filename, "rb") as f:
482 for line in f:
483 s.update(line)
484 return s.hexdigest()
485
486def preserved_envvars_exported():
487 """Variables which are taken from the environment and placed in and exported
488 from the metadata"""
489 return [
490 'BB_TASKHASH',
491 'HOME',
492 'LOGNAME',
493 'PATH',
494 'PWD',
495 'SHELL',
496 'TERM',
497 'USER',
498 ]
499
500def preserved_envvars():
501 """Variables which are taken from the environment and placed in the metadata"""
502 v = [
503 'BBPATH',
504 'BB_PRESERVE_ENV',
505 'BB_ENV_WHITELIST',
506 'BB_ENV_EXTRAWHITE',
507 ]
508 return v + preserved_envvars_exported()
509
510def filter_environment(good_vars):
511 """
512 Create a pristine environment for bitbake. This will remove variables that
513 are not known and may influence the build in a negative way.
514 """
515
516 removed_vars = {}
517 for key in os.environ.keys():
518 if key in good_vars:
519 continue
520
521 removed_vars[key] = os.environ[key]
522 os.unsetenv(key)
523 del os.environ[key]
524
525 if len(removed_vars):
526 logger.debug(1, "Removed the following variables from the environment: %s", ", ".join(removed_vars.keys()))
527
528 return removed_vars
529
530def approved_variables():
531 """
532 Determine and return the list of whitelisted variables which are approved
533 to remain in the envrionment.
534 """
535 if 'BB_PRESERVE_ENV' in os.environ:
536 return os.environ.keys()
537 approved = []
538 if 'BB_ENV_WHITELIST' in os.environ:
539 approved = os.environ['BB_ENV_WHITELIST'].split()
540 approved.extend(['BB_ENV_WHITELIST'])
541 else:
542 approved = preserved_envvars()
543 if 'BB_ENV_EXTRAWHITE' in os.environ:
544 approved.extend(os.environ['BB_ENV_EXTRAWHITE'].split())
545 if 'BB_ENV_EXTRAWHITE' not in approved:
546 approved.extend(['BB_ENV_EXTRAWHITE'])
547 return approved
548
549def clean_environment():
550 """
551 Clean up any spurious environment variables. This will remove any
552 variables the user hasn't chosen to preserve.
553 """
554 if 'BB_PRESERVE_ENV' not in os.environ:
555 good_vars = approved_variables()
556 return filter_environment(good_vars)
557
558 return {}
559
560def empty_environment():
561 """
562 Remove all variables from the environment.
563 """
564 for s in os.environ.keys():
565 os.unsetenv(s)
566 del os.environ[s]
567
568def build_environment(d):
569 """
570 Build an environment from all exported variables.
571 """
572 import bb.data
573 for var in bb.data.keys(d):
574 export = d.getVarFlag(var, "export")
575 if export:
576 os.environ[var] = d.getVar(var, True) or ""
577
578def remove(path, recurse=False):
579 """Equivalent to rm -f or rm -rf"""
580 if not path:
581 return
582 if recurse:
583 # shutil.rmtree(name) would be ideal but its too slow
584 subprocess.call(['rm', '-rf'] + glob.glob(path))
585 return
586 for name in glob.glob(path):
587 try:
588 os.unlink(name)
589 except OSError as exc:
590 if exc.errno != errno.ENOENT:
591 raise
592
593def prunedir(topdir):
594 # Delete everything reachable from the directory named in 'topdir'.
595 # CAUTION: This is dangerous!
596 for root, dirs, files in os.walk(topdir, topdown = False):
597 for name in files:
598 os.remove(os.path.join(root, name))
599 for name in dirs:
600 if os.path.islink(os.path.join(root, name)):
601 os.remove(os.path.join(root, name))
602 else:
603 os.rmdir(os.path.join(root, name))
604 os.rmdir(topdir)
605
606#
607# Could also use return re.compile("(%s)" % "|".join(map(re.escape, suffixes))).sub(lambda mo: "", var)
608# but thats possibly insane and suffixes is probably going to be small
609#
610def prune_suffix(var, suffixes, d):
611 # See if var ends with any of the suffixes listed and
612 # remove it if found
613 for suffix in suffixes:
614 if var.endswith(suffix):
615 return var.replace(suffix, "")
616 return var
617
618def mkdirhier(directory):
619 """Create a directory like 'mkdir -p', but does not complain if
620 directory already exists like os.makedirs
621 """
622
623 try:
624 os.makedirs(directory)
625 except OSError as e:
626 if e.errno != errno.EEXIST:
627 raise e
628
629def movefile(src, dest, newmtime = None, sstat = None):
630 """Moves a file from src to dest, preserving all permissions and
631 attributes; mtime will be preserved even when moving across
632 filesystems. Returns true on success and false on failure. Move is
633 atomic.
634 """
635
636 #print "movefile(" + src + "," + dest + "," + str(newmtime) + "," + str(sstat) + ")"
637 try:
638 if not sstat:
639 sstat = os.lstat(src)
640 except Exception as e:
641 print("movefile: Stating source file failed...", e)
642 return None
643
644 destexists = 1
645 try:
646 dstat = os.lstat(dest)
647 except:
648 dstat = os.lstat(os.path.dirname(dest))
649 destexists = 0
650
651 if destexists:
652 if stat.S_ISLNK(dstat[stat.ST_MODE]):
653 try:
654 os.unlink(dest)
655 destexists = 0
656 except Exception as e:
657 pass
658
659 if stat.S_ISLNK(sstat[stat.ST_MODE]):
660 try:
661 target = os.readlink(src)
662 if destexists and not stat.S_ISDIR(dstat[stat.ST_MODE]):
663 os.unlink(dest)
664 os.symlink(target, dest)
665 #os.lchown(dest,sstat[stat.ST_UID],sstat[stat.ST_GID])
666 os.unlink(src)
667 return os.lstat(dest)
668 except Exception as e:
669 print("movefile: failed to properly create symlink:", dest, "->", target, e)
670 return None
671
672 renamefailed = 1
673 if sstat[stat.ST_DEV] == dstat[stat.ST_DEV]:
674 try:
675 os.rename(src, dest)
676 renamefailed = 0
677 except Exception as e:
678 if e[0] != errno.EXDEV:
679 # Some random error.
680 print("movefile: Failed to move", src, "to", dest, e)
681 return None
682 # Invalid cross-device-link 'bind' mounted or actually Cross-Device
683
684 if renamefailed:
685 didcopy = 0
686 if stat.S_ISREG(sstat[stat.ST_MODE]):
687 try: # For safety copy then move it over.
688 shutil.copyfile(src, dest + "#new")
689 os.rename(dest + "#new", dest)
690 didcopy = 1
691 except Exception as e:
692 print('movefile: copy', src, '->', dest, 'failed.', e)
693 return None
694 else:
695 #we don't yet handle special, so we need to fall back to /bin/mv
696 a = getstatusoutput("/bin/mv -f " + "'" + src + "' '" + dest + "'")
697 if a[0] != 0:
698 print("movefile: Failed to move special file:" + src + "' to '" + dest + "'", a)
699 return None # failure
700 try:
701 if didcopy:
702 os.lchown(dest, sstat[stat.ST_UID], sstat[stat.ST_GID])
703 os.chmod(dest, stat.S_IMODE(sstat[stat.ST_MODE])) # Sticky is reset on chown
704 os.unlink(src)
705 except Exception as e:
706 print("movefile: Failed to chown/chmod/unlink", dest, e)
707 return None
708
709 if newmtime:
710 os.utime(dest, (newmtime, newmtime))
711 else:
712 os.utime(dest, (sstat[stat.ST_ATIME], sstat[stat.ST_MTIME]))
713 newmtime = sstat[stat.ST_MTIME]
714 return newmtime
715
716def copyfile(src, dest, newmtime = None, sstat = None):
717 """
718 Copies a file from src to dest, preserving all permissions and
719 attributes; mtime will be preserved even when moving across
720 filesystems. Returns true on success and false on failure.
721 """
722 #print "copyfile(" + src + "," + dest + "," + str(newmtime) + "," + str(sstat) + ")"
723 try:
724 if not sstat:
725 sstat = os.lstat(src)
726 except Exception as e:
727 logger.warn("copyfile: stat of %s failed (%s)" % (src, e))
728 return False
729
730 destexists = 1
731 try:
732 dstat = os.lstat(dest)
733 except:
734 dstat = os.lstat(os.path.dirname(dest))
735 destexists = 0
736
737 if destexists:
738 if stat.S_ISLNK(dstat[stat.ST_MODE]):
739 try:
740 os.unlink(dest)
741 destexists = 0
742 except Exception as e:
743 pass
744
745 if stat.S_ISLNK(sstat[stat.ST_MODE]):
746 try:
747 target = os.readlink(src)
748 if destexists and not stat.S_ISDIR(dstat[stat.ST_MODE]):
749 os.unlink(dest)
750 os.symlink(target, dest)
751 #os.lchown(dest,sstat[stat.ST_UID],sstat[stat.ST_GID])
752 return os.lstat(dest)
753 except Exception as e:
754 logger.warn("copyfile: failed to create symlink %s to %s (%s)" % (dest, target, e))
755 return False
756
757 if stat.S_ISREG(sstat[stat.ST_MODE]):
758 try:
759 srcchown = False
760 if not os.access(src, os.R_OK):
761 # Make sure we can read it
762 srcchown = True
763 os.chmod(src, sstat[stat.ST_MODE] | stat.S_IRUSR)
764
765 # For safety copy then move it over.
766 shutil.copyfile(src, dest + "#new")
767 os.rename(dest + "#new", dest)
768 except Exception as e:
769 logger.warn("copyfile: copy %s to %s failed (%s)" % (src, dest, e))
770 return False
771 finally:
772 if srcchown:
773 os.chmod(src, sstat[stat.ST_MODE])
774 os.utime(src, (sstat[stat.ST_ATIME], sstat[stat.ST_MTIME]))
775
776 else:
777 #we don't yet handle special, so we need to fall back to /bin/mv
778 a = getstatusoutput("/bin/cp -f " + "'" + src + "' '" + dest + "'")
779 if a[0] != 0:
780 logger.warn("copyfile: failed to copy special file %s to %s (%s)" % (src, dest, a))
781 return False # failure
782 try:
783 os.lchown(dest, sstat[stat.ST_UID], sstat[stat.ST_GID])
784 os.chmod(dest, stat.S_IMODE(sstat[stat.ST_MODE])) # Sticky is reset on chown
785 except Exception as e:
786 logger.warn("copyfile: failed to chown/chmod %s (%s)" % (dest, e))
787 return False
788
789 if newmtime:
790 os.utime(dest, (newmtime, newmtime))
791 else:
792 os.utime(dest, (sstat[stat.ST_ATIME], sstat[stat.ST_MTIME]))
793 newmtime = sstat[stat.ST_MTIME]
794 return newmtime
795
796def which(path, item, direction = 0):
797 """
798 Locate a file in a PATH
799 """
800
801 paths = (path or "").split(':')
802 if direction != 0:
803 paths.reverse()
804
805 for p in paths:
806 next = os.path.join(p, item)
807 if os.path.exists(next):
808 if not os.path.isabs(next):
809 next = os.path.abspath(next)
810 return next
811
812 return ""
813
814def to_boolean(string, default=None):
815 if not string:
816 return default
817
818 normalized = string.lower()
819 if normalized in ("y", "yes", "1", "true"):
820 return True
821 elif normalized in ("n", "no", "0", "false"):
822 return False
823 else:
824 raise ValueError("Invalid value for to_boolean: %s" % string)
825
826def contains(variable, checkvalues, truevalue, falsevalue, d):
827 val = d.getVar(variable, True)
828 if not val:
829 return falsevalue
830 val = set(val.split())
831 if isinstance(checkvalues, basestring):
832 checkvalues = set(checkvalues.split())
833 else:
834 checkvalues = set(checkvalues)
835 if checkvalues.issubset(val):
836 return truevalue
837 return falsevalue
838
839def cpu_count():
840 return multiprocessing.cpu_count()
841
842def nonblockingfd(fd):
843 fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK)
844
845def process_profilelog(fn):
846 # Redirect stdout to capture profile information
847 pout = open(fn + '.processed', 'w')
848 so = sys.stdout.fileno()
849 orig_so = os.dup(sys.stdout.fileno())
850 os.dup2(pout.fileno(), so)
851
852 import pstats
853 p = pstats.Stats(fn)
854 p.sort_stats('time')
855 p.print_stats()
856 p.print_callers()
857 p.sort_stats('cumulative')
858 p.print_stats()
859
860 os.dup2(orig_so, so)
861 pout.flush()
862 pout.close()
863
864#
865# Was present to work around multiprocessing pool bugs in python < 2.7.3
866#
867def multiprocessingpool(*args, **kwargs):
868 return multiprocessing.Pool(*args, **kwargs)
869