blob: 408ad44e6e4b5f4c2aff4fbdc17258ec9a6ac689 [file] [log] [blame]
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001#
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08002# BitBake Toaster Implementation
3#
4# Copyright (C) 2018 Wind River Systems
5#
Brad Bishopc342db32019-05-15 21:57:59 -04006# SPDX-License-Identifier: GPL-2.0-only
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08007#
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08008
9# buildimport: import a project for project specific configuration
10#
11# Usage:
12# (a) Set up Toaster environent
13#
14# (b) Call buildimport
15# $ /path/to/bitbake/lib/toaster/manage.py buildimport \
16# --name=$PROJECTNAME \
17# --path=$BUILD_DIRECTORY \
18# --callback="$CALLBACK_SCRIPT" \
19# --command="configure|reconfigure|import"
20#
21# (c) Return is "|Default_image=%s|Project_id=%d"
22#
23# (d) Open Toaster to this project using for example:
24# $ xdg-open http://localhost:$toaster_port/toastergui/project_specific/$project_id
25#
26# (e) To delete a project:
27# $ /path/to/bitbake/lib/toaster/manage.py buildimport \
28# --name=$PROJECTNAME --delete-project
29#
30
31
32# ../bitbake/lib/toaster/manage.py buildimport --name=test --path=`pwd` --callback="" --command=import
33
34from django.core.management.base import BaseCommand, CommandError
35from django.core.exceptions import ObjectDoesNotExist
36from orm.models import ProjectManager, Project, Release, ProjectVariable
37from orm.models import Layer, Layer_Version, LayerSource, ProjectLayer
38from toastergui.api import scan_layer_content
39from django.db import OperationalError
40
41import os
42import re
43import os.path
44import subprocess
45import shutil
46
47# Toaster variable section delimiters
48TOASTER_PROLOG = '#=== TOASTER_CONFIG_PROLOG ==='
49TOASTER_EPILOG = '#=== TOASTER_CONFIG_EPILOG ==='
50
51# quick development/debugging support
52verbose = 2
53def _log(msg):
54 if 1 == verbose:
55 print(msg)
56 elif 2 == verbose:
57 f1=open('/tmp/toaster.log', 'a')
58 f1.write("|" + msg + "|\n" )
59 f1.close()
60
61
62__config_regexp__ = re.compile( r"""
63 ^
64 (?P<exp>export\s+)?
65 (?P<var>[a-zA-Z0-9\-_+.${}/~]+?)
66 (\[(?P<flag>[a-zA-Z0-9\-_+.]+)\])?
67
68 \s* (
69 (?P<colon>:=) |
70 (?P<lazyques>\?\?=) |
71 (?P<ques>\?=) |
72 (?P<append>\+=) |
73 (?P<prepend>=\+) |
74 (?P<predot>=\.) |
75 (?P<postdot>\.=) |
76 =
77 ) \s*
78
79 (?!'[^']*'[^']*'$)
80 (?!\"[^\"]*\"[^\"]*\"$)
81 (?P<apo>['\"])
82 (?P<value>.*)
83 (?P=apo)
84 $
85 """, re.X)
86
87class Command(BaseCommand):
88 args = "<name> <path> <release>"
89 help = "Import a command line build directory"
90 vars = {}
91 toaster_vars = {}
92
93 def add_arguments(self, parser):
94 parser.add_argument(
95 '--name', dest='name', required=True,
96 help='name of the project',
97 )
98 parser.add_argument(
99 '--path', dest='path', required=True,
100 help='path to the project',
101 )
102 parser.add_argument(
103 '--release', dest='release', required=False,
104 help='release for the project',
105 )
106 parser.add_argument(
107 '--callback', dest='callback', required=False,
108 help='callback for project config update',
109 )
110 parser.add_argument(
111 '--delete-project', dest='delete_project', required=False,
112 help='delete this project from the database',
113 )
114 parser.add_argument(
115 '--command', dest='command', required=False,
116 help='command (configure,reconfigure,import)',
117 )
118
119 # Extract the bb variables from a conf file
120 def scan_conf(self,fn):
121 vars = self.vars
122 toaster_vars = self.toaster_vars
123
124 #_log("scan_conf:%s" % fn)
125 if not os.path.isfile(fn):
126 return
127 f = open(fn, 'r')
128
129 #statements = ast.StatementGroup()
130 lineno = 0
131 is_toaster_section = False
132 while True:
133 lineno = lineno + 1
134 s = f.readline()
135 if not s:
136 break
137 w = s.strip()
138 # skip empty lines
139 if not w:
140 continue
141 # evaluate Toaster sections
142 if w.startswith(TOASTER_PROLOG):
143 is_toaster_section = True
144 continue
145 if w.startswith(TOASTER_EPILOG):
146 is_toaster_section = False
147 continue
148 s = s.rstrip()
149 while s[-1] == '\\':
150 s2 = f.readline().strip()
151 lineno = lineno + 1
152 if (not s2 or s2 and s2[0] != "#") and s[0] == "#" :
153 echo("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))
154 s = s[:-1] + s2
155 # skip comments
156 if s[0] == '#':
157 continue
158 # process the line for just assignments
159 m = __config_regexp__.match(s)
160 if m:
161 groupd = m.groupdict()
162 var = groupd['var']
163 value = groupd['value']
164
165 if groupd['lazyques']:
166 if not var in vars:
167 vars[var] = value
168 continue
169 if groupd['ques']:
170 if not var in vars:
171 vars[var] = value
172 continue
173 # preset empty blank for remaining operators
174 if not var in vars:
175 vars[var] = ''
176 if groupd['append']:
177 vars[var] += value
178 elif groupd['prepend']:
179 vars[var] = "%s%s" % (value,vars[var])
180 elif groupd['predot']:
181 vars[var] = "%s %s" % (value,vars[var])
182 elif groupd['postdot']:
183 vars[var] = "%s %s" % (vars[var],value)
184 else:
185 vars[var] = "%s" % (value)
186 # capture vars in a Toaster section
187 if is_toaster_section:
188 toaster_vars[var] = vars[var]
189
190 # DONE WITH PARSING
191 f.close()
192 self.vars = vars
193 self.toaster_vars = toaster_vars
194
195 # Update the scanned project variables
196 def update_project_vars(self,project,name):
197 pv, create = ProjectVariable.objects.get_or_create(project = project, name = name)
198 if (not name in self.vars.keys()) or (not self.vars[name]):
199 self.vars[name] = pv.value
200 else:
201 if pv.value != self.vars[name]:
202 pv.value = self.vars[name]
203 pv.save()
204
205 # Find the git version of the installation
206 def find_layer_dir_version(self,path):
207 # * rocko ...
208
209 install_version = ''
210 cwd = os.getcwd()
211 os.chdir(path)
212 p = subprocess.Popen(['git', 'branch', '-av'], stdout=subprocess.PIPE,
213 stderr=subprocess.PIPE)
214 out, err = p.communicate()
215 out = out.decode("utf-8")
216 for branch in out.split('\n'):
217 if ('*' == branch[0:1]) and ('no branch' not in branch):
218 install_version = re.sub(' .*','',branch[2:])
219 break
220 if 'remotes/m/master' in branch:
221 install_version = re.sub('.*base/','',branch)
222 break
223 os.chdir(cwd)
224 return install_version
225
226 # Compute table of the installation's registered layer versions (branch or commit)
227 def find_layer_dir_versions(self,INSTALL_URL_PREFIX):
228 lv_dict = {}
229 layer_versions = Layer_Version.objects.all()
230 for lv in layer_versions:
231 layer = Layer.objects.filter(pk=lv.layer.pk)[0]
232 if layer.vcs_url:
233 url_short = layer.vcs_url.replace(INSTALL_URL_PREFIX,'')
234 else:
235 url_short = ''
236 # register the core, branch, and the version variations
237 lv_dict["%s,%s,%s" % (url_short,lv.dirpath,'')] = (lv.id,layer.name)
238 lv_dict["%s,%s,%s" % (url_short,lv.dirpath,lv.branch)] = (lv.id,layer.name)
239 lv_dict["%s,%s,%s" % (url_short,lv.dirpath,lv.commit)] = (lv.id,layer.name)
240 #_log(" (%s,%s,%s|%s) = (%s,%s)" % (url_short,lv.dirpath,lv.branch,lv.commit,lv.id,layer.name))
241 return lv_dict
242
243 # Apply table of all layer versions
244 def extract_bblayers(self):
245 # set up the constants
246 bblayer_str = self.vars['BBLAYERS']
247 TOASTER_DIR = os.environ.get('TOASTER_DIR')
248 INSTALL_CLONE_PREFIX = os.path.dirname(TOASTER_DIR) + "/"
249 TOASTER_CLONE_PREFIX = TOASTER_DIR + "/_toaster_clones/"
250 INSTALL_URL_PREFIX = ''
251 layers = Layer.objects.filter(name='openembedded-core')
252 for layer in layers:
253 if layer.vcs_url:
254 INSTALL_URL_PREFIX = layer.vcs_url
255 break
256 INSTALL_URL_PREFIX = INSTALL_URL_PREFIX.replace("/poky","/")
257 INSTALL_VERSION_DIR = TOASTER_DIR
258 INSTALL_URL_POSTFIX = INSTALL_URL_PREFIX.replace(':','_')
259 INSTALL_URL_POSTFIX = INSTALL_URL_POSTFIX.replace('/','_')
260 INSTALL_URL_POSTFIX = "%s_%s" % (TOASTER_CLONE_PREFIX,INSTALL_URL_POSTFIX)
261
262 # get the set of available layer:layer_versions
263 lv_dict = self.find_layer_dir_versions(INSTALL_URL_PREFIX)
264
265 # compute the layer matches
266 layers_list = []
267 for line in bblayer_str.split(' '):
268 if not line:
269 continue
270 if line.endswith('/local'):
271 continue
272
273 # isolate the repo
274 layer_path = line
275 line = line.replace(INSTALL_URL_POSTFIX,'').replace(INSTALL_CLONE_PREFIX,'').replace('/layers/','/').replace('/poky/','/')
276
277 # isolate the sub-path
278 path_index = line.rfind('/')
279 if path_index > 0:
280 sub_path = line[path_index+1:]
281 line = line[0:path_index]
282 else:
283 sub_path = ''
284
285 # isolate the version
286 if TOASTER_CLONE_PREFIX in layer_path:
287 is_toaster_clone = True
288 # extract version from name syntax
289 version_index = line.find('_')
290 if version_index > 0:
291 version = line[version_index+1:]
292 line = line[0:version_index]
293 else:
294 version = ''
295 _log("TOASTER_CLONE(%s/%s), version=%s" % (line,sub_path,version))
296 else:
297 is_toaster_clone = False
298 # version is from the installation
299 version = self.find_layer_dir_version(layer_path)
300 _log("LOCAL_CLONE(%s/%s), version=%s" % (line,sub_path,version))
301
302 # capture the layer information into layers_list
303 layers_list.append( (line,sub_path,version,layer_path,is_toaster_clone) )
304 return layers_list,lv_dict
305
306 #
307 def find_import_release(self,layers_list,lv_dict,default_release):
308 # poky,meta,rocko => 4;openembedded-core
309 release = default_release
310 for line,path,version,layer_path,is_toaster_clone in layers_list:
311 key = "%s,%s,%s" % (line,path,version)
312 if key in lv_dict:
313 lv_id = lv_dict[key]
314 if 'openembedded-core' == lv_id[1]:
315 _log("Find_import_release(%s):version=%s,Toaster=%s" % (lv_id[1],version,is_toaster_clone))
316 # only versions in Toaster managed layers are accepted
317 if not is_toaster_clone:
318 break
319 try:
320 release = Release.objects.get(name=version)
321 except:
322 pass
323 break
324 _log("Find_import_release:RELEASE=%s" % release.name)
325 return release
326
327 # Apply the found conf layers
328 def apply_conf_bblayers(self,layers_list,lv_dict,project,release=None):
329 for line,path,version,layer_path,is_toaster_clone in layers_list:
330 # Assert release promote if present
331 if release:
332 version = release
333 # try to match the key to a layer_version
334 key = "%s,%s,%s" % (line,path,version)
335 key_short = "%s,%s,%s" % (line,path,'')
336 lv_id = ''
337 if key in lv_dict:
338 lv_id = lv_dict[key]
339 lv = Layer_Version.objects.get(pk=int(lv_id[0]))
340 pl,created = ProjectLayer.objects.get_or_create(project=project,
341 layercommit=lv)
342 pl.optional=False
343 pl.save()
344 _log(" %s => %s;%s" % (key,lv_id[0],lv_id[1]))
345 elif key_short in lv_dict:
346 lv_id = lv_dict[key_short]
347 lv = Layer_Version.objects.get(pk=int(lv_id[0]))
348 pl,created = ProjectLayer.objects.get_or_create(project=project,
349 layercommit=lv)
350 pl.optional=False
351 pl.save()
352 _log(" %s ?> %s" % (key,lv_dict[key_short]))
353 else:
354 _log("%s <= %s" % (key,layer_path))
355 found = False
356 # does local layer already exist in this project?
357 try:
358 for pl in ProjectLayer.objects.filter(project=project):
359 if pl.layercommit.layer.local_source_dir == layer_path:
360 found = True
361 _log(" Project Local Layer found!")
362 except Exception as e:
363 _log("ERROR: Local Layer '%s'" % e)
364 pass
365
366 if not found:
367 # Does Layer name+path already exist?
368 try:
369 layer_name_base = os.path.basename(layer_path)
370 _log("Layer_lookup: try '%s','%s'" % (layer_name_base,layer_path))
371 layer = Layer.objects.get(name=layer_name_base,local_source_dir = layer_path)
372 # Found! Attach layer_version and ProjectLayer
373 layer_version = Layer_Version.objects.create(
374 layer=layer,
375 project=project,
376 layer_source=LayerSource.TYPE_IMPORTED)
377 layer_version.save()
378 pl,created = ProjectLayer.objects.get_or_create(project=project,
379 layercommit=layer_version)
380 pl.optional=False
381 pl.save()
382 found = True
383 # add layer contents to this layer version
384 scan_layer_content(layer,layer_version)
385 _log(" Parent Local Layer found in db!")
386 except Exception as e:
387 _log("Layer_exists_test_failed: Local Layer '%s'" % e)
388 pass
389
390 if not found:
391 # Insure that layer path exists, in case of user typo
392 if not os.path.isdir(layer_path):
393 _log("ERROR:Layer path '%s' not found" % layer_path)
394 continue
395 # Add layer to db and attach project to it
396 layer_name_base = os.path.basename(layer_path)
397 # generate a unique layer name
398 layer_name_matches = {}
399 for layer in Layer.objects.filter(name__contains=layer_name_base):
400 layer_name_matches[layer.name] = '1'
401 layer_name_idx = 0
402 layer_name_test = layer_name_base
403 while layer_name_test in layer_name_matches.keys():
404 layer_name_idx += 1
405 layer_name_test = "%s_%d" % (layer_name_base,layer_name_idx)
406 # create the layer and layer_verion objects
407 layer = Layer.objects.create(name=layer_name_test)
408 layer.local_source_dir = layer_path
409 layer_version = Layer_Version.objects.create(
410 layer=layer,
411 project=project,
412 layer_source=LayerSource.TYPE_IMPORTED)
413 layer.save()
414 layer_version.save()
415 pl,created = ProjectLayer.objects.get_or_create(project=project,
416 layercommit=layer_version)
417 pl.optional=False
418 pl.save()
419 # register the layer's content
420 _log(" Local Layer Add content")
421 scan_layer_content(layer,layer_version)
422 _log(" Local Layer Added '%s'!" % layer_name_test)
423
424 # Scan the project's conf files (if any)
425 def scan_conf_variables(self,project_path):
426 # scan the project's settings, add any new layers or variables
427 if os.path.isfile("%s/conf/local.conf" % project_path):
428 self.scan_conf("%s/conf/local.conf" % project_path)
429 self.scan_conf("%s/conf/bblayers.conf" % project_path)
430 # Import then disable old style Toaster conf files (before 'merged_attr')
431 old_toaster_local = "%s/conf/toaster.conf" % project_path
432 if os.path.isfile(old_toaster_local):
433 self.scan_conf(old_toaster_local)
434 shutil.move(old_toaster_local, old_toaster_local+"_old")
435 old_toaster_layer = "%s/conf/toaster-bblayers.conf" % project_path
436 if os.path.isfile(old_toaster_layer):
437 self.scan_conf(old_toaster_layer)
438 shutil.move(old_toaster_layer, old_toaster_layer+"_old")
439
440 # Scan the found conf variables (if any)
441 def apply_conf_variables(self,project,layers_list,lv_dict,release=None):
442 if self.vars:
443 # Catch vars relevant to Toaster (in case no Toaster section)
444 self.update_project_vars(project,'DISTRO')
445 self.update_project_vars(project,'MACHINE')
446 self.update_project_vars(project,'IMAGE_INSTALL_append')
447 self.update_project_vars(project,'IMAGE_FSTYPES')
448 self.update_project_vars(project,'PACKAGE_CLASSES')
449 # These vars are typically only assigned by Toaster
450 #self.update_project_vars(project,'DL_DIR')
451 #self.update_project_vars(project,'SSTATE_DIR')
452
453 # Assert found Toaster vars
454 for var in self.toaster_vars.keys():
455 pv, create = ProjectVariable.objects.get_or_create(project = project, name = var)
456 pv.value = self.toaster_vars[var]
457 _log("* Add/update Toaster var '%s' = '%s'" % (pv.name,pv.value))
458 pv.save()
459
460 # Assert found BBLAYERS
461 if 0 < verbose:
462 for pl in ProjectLayer.objects.filter(project=project):
463 release_name = 'None' if not pl.layercommit.release else pl.layercommit.release.name
464 print(" BEFORE:ProjectLayer=%s,%s,%s,%s" % (pl.layercommit.layer.name,release_name,pl.layercommit.branch,pl.layercommit.commit))
465 self.apply_conf_bblayers(layers_list,lv_dict,project,release)
466 if 0 < verbose:
467 for pl in ProjectLayer.objects.filter(project=project):
468 release_name = 'None' if not pl.layercommit.release else pl.layercommit.release.name
469 print(" AFTER :ProjectLayer=%s,%s,%s,%s" % (pl.layercommit.layer.name,release_name,pl.layercommit.branch,pl.layercommit.commit))
470
471
472 def handle(self, *args, **options):
473 project_name = options['name']
474 project_path = options['path']
475 project_callback = options['callback'] if options['callback'] else ''
476 release_name = options['release'] if options['release'] else ''
477
478 #
479 # Delete project
480 #
481
482 if options['delete_project']:
483 try:
484 print("Project '%s' delete from Toaster database" % (project_name))
485 project = Project.objects.get(name=project_name)
486 # TODO: deep project delete
487 project.delete()
488 print("Project '%s' Deleted" % (project_name))
489 return
490 except Exception as e:
491 print("Project '%s' not found, not deleted (%s)" % (project_name,e))
492 return
493
494 #
495 # Create/Update/Import project
496 #
497
498 # See if project (by name) exists
499 project = None
500 try:
501 # Project already exists
502 project = Project.objects.get(name=project_name)
503 except Exception as e:
504 pass
505
506 # Find the installation's default release
507 default_release = Release.objects.get(id=1)
508
509 # SANITY: if 'reconfig' but project does not exist (deleted externally), switch to 'import'
510 if ("reconfigure" == options['command']) and (None == project):
511 options['command'] = 'import'
512
513 # 'Configure':
514 if "configure" == options['command']:
515 # Note: ignore any existing conf files
516 # create project, SANITY: reuse any project of same name
517 project = Project.objects.create_project(project_name,default_release,project)
518
519 # 'Re-configure':
520 if "reconfigure" == options['command']:
521 # Scan the directory's conf files
522 self.scan_conf_variables(project_path)
523 # Scan the layer list
524 layers_list,lv_dict = self.extract_bblayers()
525 # Apply any new layers or variables
526 self.apply_conf_variables(project,layers_list,lv_dict)
527
528 # 'Import':
529 if "import" == options['command']:
530 # Scan the directory's conf files
531 self.scan_conf_variables(project_path)
532 # Remove these Toaster controlled variables
533 for var in ('DL_DIR','SSTATE_DIR'):
534 self.vars.pop(var, None)
535 self.toaster_vars.pop(var, None)
536 # Scan the layer list
537 layers_list,lv_dict = self.extract_bblayers()
538 # Find the directory's release, and promote to default_release if local paths
539 release = self.find_import_release(layers_list,lv_dict,default_release)
540 # create project, SANITY: reuse any project of same name
541 project = Project.objects.create_project(project_name,release,project)
542 # Apply any new layers or variables
543 self.apply_conf_variables(project,layers_list,lv_dict,release)
544 # WORKAROUND: since we now derive the release, redirect 'newproject_specific' to 'project_specific'
545 project.set_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE','1')
546
547 # Set up the project's meta data
548 project.builddir = project_path
549 project.merged_attr = True
550 project.set_variable(Project.PROJECT_SPECIFIC_CALLBACK,project_callback)
551 project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_EDIT)
552 if ("configure" == options['command']) or ("import" == options['command']):
553 # preset the mode and default image recipe
554 project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,Project.PROJECT_SPECIFIC_NEW)
555 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,"core-image-minimal")
556 # Assert any extended/custom actions or variables for new non-Toaster projects
557 if not len(self.toaster_vars):
558 pass
559 else:
560 project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,Project.PROJECT_SPECIFIC_NONE)
561
562 # Save the updated Project
563 project.save()
564
565 _log("Buildimport:project='%s' at '%d'" % (project_name,project.id))
566
567 if ('DEFAULT_IMAGE' in self.vars) and (self.vars['DEFAULT_IMAGE']):
568 print("|Default_image=%s|Project_id=%d" % (self.vars['DEFAULT_IMAGE'],project.id))
569 else:
570 print("|Project_id=%d" % (project.id))
571