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