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