summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bitbake/lib/toaster/bldcontrol/localhostbecontroller.py172
-rw-r--r--bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py2
-rw-r--r--bitbake/lib/toaster/orm/migrations/0018_project_specific.py28
-rw-r--r--bitbake/lib/toaster/orm/models.py61
-rw-r--r--bitbake/lib/toaster/toastergui/api.py168
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/layerBtn.js12
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/libtoaster.js105
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/mrbsection.js4
-rw-r--r--bitbake/lib/toaster/toastergui/static/js/projecttopbar.js22
-rw-r--r--bitbake/lib/toaster/toastergui/tables.py4
-rw-r--r--bitbake/lib/toaster/toastergui/templates/base_specific.html128
-rw-r--r--bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html48
-rw-r--r--bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html2
-rw-r--r--bitbake/lib/toaster/toastergui/templates/importlayer.html4
-rw-r--r--bitbake/lib/toaster/toastergui/templates/landing_specific.html50
-rw-r--r--bitbake/lib/toaster/toastergui/templates/layerdetails.html3
-rw-r--r--bitbake/lib/toaster/toastergui/templates/mrb_section.html2
-rw-r--r--bitbake/lib/toaster/toastergui/templates/newcustomimage.html4
-rw-r--r--bitbake/lib/toaster/toastergui/templates/newproject_specific.html95
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project.html7
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project_specific.html162
-rw-r--r--bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html80
-rw-r--r--bitbake/lib/toaster/toastergui/templates/projectconf.html7
-rw-r--r--bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html23
-rw-r--r--bitbake/lib/toaster/toastergui/urls.py13
-rw-r--r--[-rwxr-xr-x]bitbake/lib/toaster/toastergui/views.py151
-rw-r--r--bitbake/lib/toaster/toastergui/widgets.py6
-rw-r--r--bitbake/lib/toaster/toastermain/management/commands/builddelete.py6
-rw-r--r--bitbake/lib/toaster/toastermain/management/commands/buildimport.py586
29 files changed, 1900 insertions, 55 deletions
diff --git a/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py b/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py
index 16c7c80441..6bdd743b8b 100644
--- a/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py
+++ b/bitbake/lib/toaster/bldcontrol/localhostbecontroller.py
@@ -27,8 +27,9 @@ import shutil
27import time 27import time
28from django.db import transaction 28from django.db import transaction
29from django.db.models import Q 29from django.db.models import Q
30from bldcontrol.models import BuildEnvironment, BRLayer, BRVariable, BRTarget, BRBitbake 30from bldcontrol.models import BuildEnvironment, BuildRequest, BRLayer, BRVariable, BRTarget, BRBitbake, Build
31from orm.models import CustomImageRecipe, Layer, Layer_Version, ProjectLayer, ToasterSetting 31from orm.models import CustomImageRecipe, Layer, Layer_Version, Project, ProjectLayer, ToasterSetting
32from orm.models import signal_runbuilds
32import subprocess 33import subprocess
33 34
34from toastermain import settings 35from toastermain import settings
@@ -38,6 +39,8 @@ from bldcontrol.bbcontroller import BuildEnvironmentController, ShellCmdExceptio
38import logging 39import logging
39logger = logging.getLogger("toaster") 40logger = logging.getLogger("toaster")
40 41
42install_dir = os.environ.get('TOASTER_DIR')
43
41from pprint import pprint, pformat 44from pprint import pprint, pformat
42 45
43class LocalhostBEController(BuildEnvironmentController): 46class LocalhostBEController(BuildEnvironmentController):
@@ -87,10 +90,10 @@ class LocalhostBEController(BuildEnvironmentController):
87 #logger.debug("localhostbecontroller: using HEAD checkout in %s" % local_checkout_path) 90 #logger.debug("localhostbecontroller: using HEAD checkout in %s" % local_checkout_path)
88 return local_checkout_path 91 return local_checkout_path
89 92
90 93 def setCloneStatus(self,bitbake,status,total,current,repo_name):
91 def setCloneStatus(self,bitbake,status,total,current):
92 bitbake.req.build.repos_cloned=current 94 bitbake.req.build.repos_cloned=current
93 bitbake.req.build.repos_to_clone=total 95 bitbake.req.build.repos_to_clone=total
96 bitbake.req.build.progress_item=repo_name
94 bitbake.req.build.save() 97 bitbake.req.build.save()
95 98
96 def setLayers(self, bitbake, layers, targets): 99 def setLayers(self, bitbake, layers, targets):
@@ -100,6 +103,7 @@ class LocalhostBEController(BuildEnvironmentController):
100 103
101 layerlist = [] 104 layerlist = []
102 nongitlayerlist = [] 105 nongitlayerlist = []
106 layer_index = 0
103 git_env = os.environ.copy() 107 git_env = os.environ.copy()
104 # (note: add custom environment settings here) 108 # (note: add custom environment settings here)
105 109
@@ -113,7 +117,7 @@ class LocalhostBEController(BuildEnvironmentController):
113 if bitbake.giturl and bitbake.commit: 117 if bitbake.giturl and bitbake.commit:
114 gitrepos[(bitbake.giturl, bitbake.commit)] = [] 118 gitrepos[(bitbake.giturl, bitbake.commit)] = []
115 gitrepos[(bitbake.giturl, bitbake.commit)].append( 119 gitrepos[(bitbake.giturl, bitbake.commit)].append(
116 ("bitbake", bitbake.dirpath)) 120 ("bitbake", bitbake.dirpath, 0))
117 121
118 for layer in layers: 122 for layer in layers:
119 # We don't need to git clone the layer for the CustomImageRecipe 123 # We don't need to git clone the layer for the CustomImageRecipe
@@ -124,12 +128,13 @@ class LocalhostBEController(BuildEnvironmentController):
124 # If we have local layers then we don't need clone them 128 # If we have local layers then we don't need clone them
125 # For local layers giturl will be empty 129 # For local layers giturl will be empty
126 if not layer.giturl: 130 if not layer.giturl:
127 nongitlayerlist.append(layer.layer_version.layer.local_source_dir) 131 nongitlayerlist.append( "%03d:%s" % (layer_index,layer.local_source_dir) )
128 continue 132 continue
129 133
130 if not (layer.giturl, layer.commit) in gitrepos: 134 if not (layer.giturl, layer.commit) in gitrepos:
131 gitrepos[(layer.giturl, layer.commit)] = [] 135 gitrepos[(layer.giturl, layer.commit)] = []
132 gitrepos[(layer.giturl, layer.commit)].append( (layer.name, layer.dirpath) ) 136 gitrepos[(layer.giturl, layer.commit)].append( (layer.name,layer.dirpath,layer_index) )
137 layer_index += 1
133 138
134 139
135 logger.debug("localhostbecontroller, our git repos are %s" % pformat(gitrepos)) 140 logger.debug("localhostbecontroller, our git repos are %s" % pformat(gitrepos))
@@ -159,9 +164,9 @@ class LocalhostBEController(BuildEnvironmentController):
159 # 3. checkout the repositories 164 # 3. checkout the repositories
160 clone_count=0 165 clone_count=0
161 clone_total=len(gitrepos.keys()) 166 clone_total=len(gitrepos.keys())
162 self.setCloneStatus(bitbake,'Started',clone_total,clone_count) 167 self.setCloneStatus(bitbake,'Started',clone_total,clone_count,'')
163 for giturl, commit in gitrepos.keys(): 168 for giturl, commit in gitrepos.keys():
164 self.setCloneStatus(bitbake,'progress',clone_total,clone_count) 169 self.setCloneStatus(bitbake,'progress',clone_total,clone_count,gitrepos[(giturl, commit)][0][0])
165 clone_count += 1 170 clone_count += 1
166 171
167 localdirname = os.path.join(self.be.sourcedir, self.getGitCloneDirectory(giturl, commit)) 172 localdirname = os.path.join(self.be.sourcedir, self.getGitCloneDirectory(giturl, commit))
@@ -205,16 +210,16 @@ class LocalhostBEController(BuildEnvironmentController):
205 self._shellcmd("git clone -b \"%s\" \"%s\" \"%s\" " % (bitbake.commit, bitbake.giturl, os.path.join(self.pokydirname, 'bitbake')),env=git_env) 210 self._shellcmd("git clone -b \"%s\" \"%s\" \"%s\" " % (bitbake.commit, bitbake.giturl, os.path.join(self.pokydirname, 'bitbake')),env=git_env)
206 211
207 # verify our repositories 212 # verify our repositories
208 for name, dirpath in gitrepos[(giturl, commit)]: 213 for name, dirpath, index in gitrepos[(giturl, commit)]:
209 localdirpath = os.path.join(localdirname, dirpath) 214 localdirpath = os.path.join(localdirname, dirpath)
210 logger.debug("localhostbecontroller: localdirpath expected '%s'" % localdirpath) 215 logger.debug("localhostbecontroller: localdirpath expects '%s'" % localdirpath)
211 if not os.path.exists(localdirpath): 216 if not os.path.exists(localdirpath):
212 raise BuildSetupException("Cannot find layer git path '%s' in checked out repository '%s:%s'. Aborting." % (localdirpath, giturl, commit)) 217 raise BuildSetupException("Cannot find layer git path '%s' in checked out repository '%s:%s'. Aborting." % (localdirpath, giturl, commit))
213 218
214 if name != "bitbake": 219 if name != "bitbake":
215 layerlist.append(localdirpath.rstrip("/")) 220 layerlist.append("%03d:%s" % (index,localdirpath.rstrip("/")))
216 221
217 self.setCloneStatus(bitbake,'complete',clone_total,clone_count) 222 self.setCloneStatus(bitbake,'complete',clone_total,clone_count,'')
218 logger.debug("localhostbecontroller: current layer list %s " % pformat(layerlist)) 223 logger.debug("localhostbecontroller: current layer list %s " % pformat(layerlist))
219 224
220 if self.pokydirname is None and os.path.exists(os.path.join(self.be.sourcedir, "oe-init-build-env")): 225 if self.pokydirname is None and os.path.exists(os.path.join(self.be.sourcedir, "oe-init-build-env")):
@@ -232,7 +237,7 @@ class LocalhostBEController(BuildEnvironmentController):
232 customrecipe, layers) 237 customrecipe, layers)
233 238
234 if os.path.isdir(custom_layer_path): 239 if os.path.isdir(custom_layer_path):
235 layerlist.append(custom_layer_path) 240 layerlist.append("%03d:%s" % (layer_index,custom_layer_path))
236 241
237 except CustomImageRecipe.DoesNotExist: 242 except CustomImageRecipe.DoesNotExist:
238 continue # not a custom recipe, skip 243 continue # not a custom recipe, skip
@@ -240,7 +245,11 @@ class LocalhostBEController(BuildEnvironmentController):
240 layerlist.extend(nongitlayerlist) 245 layerlist.extend(nongitlayerlist)
241 logger.debug("\n\nset layers gives this list %s" % pformat(layerlist)) 246 logger.debug("\n\nset layers gives this list %s" % pformat(layerlist))
242 self.islayerset = True 247 self.islayerset = True
243 return layerlist 248
249 # restore the order of layer list for bblayers.conf
250 layerlist.sort()
251 sorted_layerlist = [l[4:] for l in layerlist]
252 return sorted_layerlist
244 253
245 def setup_custom_image_recipe(self, customrecipe, layers): 254 def setup_custom_image_recipe(self, customrecipe, layers):
246 """ Set up toaster-custom-images layer and recipe files """ 255 """ Set up toaster-custom-images layer and recipe files """
@@ -310,31 +319,115 @@ class LocalhostBEController(BuildEnvironmentController):
310 319
311 def triggerBuild(self, bitbake, layers, variables, targets, brbe): 320 def triggerBuild(self, bitbake, layers, variables, targets, brbe):
312 layers = self.setLayers(bitbake, layers, targets) 321 layers = self.setLayers(bitbake, layers, targets)
322 is_merged_attr = bitbake.req.project.merged_attr
323
324 git_env = os.environ.copy()
325 # (note: add custom environment settings here)
326 try:
327 # insure that the project init/build uses the selected bitbake, and not Toaster's
328 del git_env['TEMPLATECONF']
329 del git_env['BBBASEDIR']
330 del git_env['BUILDDIR']
331 except KeyError:
332 pass
313 333
314 # init build environment from the clone 334 # init build environment from the clone
315 builddir = '%s-toaster-%d' % (self.be.builddir, bitbake.req.project.id) 335 if bitbake.req.project.builddir:
336 builddir = bitbake.req.project.builddir
337 else:
338 builddir = '%s-toaster-%d' % (self.be.builddir, bitbake.req.project.id)
316 oe_init = os.path.join(self.pokydirname, 'oe-init-build-env') 339 oe_init = os.path.join(self.pokydirname, 'oe-init-build-env')
317 # init build environment 340 # init build environment
318 try: 341 try:
319 custom_script = ToasterSetting.objects.get(name="CUSTOM_BUILD_INIT_SCRIPT").value 342 custom_script = ToasterSetting.objects.get(name="CUSTOM_BUILD_INIT_SCRIPT").value
320 custom_script = custom_script.replace("%BUILDDIR%" ,builddir) 343 custom_script = custom_script.replace("%BUILDDIR%" ,builddir)
321 self._shellcmd("bash -c 'source %s'" % (custom_script)) 344 self._shellcmd("bash -c 'source %s'" % (custom_script),env=git_env)
322 except ToasterSetting.DoesNotExist: 345 except ToasterSetting.DoesNotExist:
323 self._shellcmd("bash -c 'source %s %s'" % (oe_init, builddir), 346 self._shellcmd("bash -c 'source %s %s'" % (oe_init, builddir),
324 self.be.sourcedir) 347 self.be.sourcedir,env=git_env)
325 348
326 # update bblayers.conf 349 # update bblayers.conf
327 bblconfpath = os.path.join(builddir, "conf/toaster-bblayers.conf") 350 if not is_merged_attr:
328 with open(bblconfpath, 'w') as bblayers: 351 bblconfpath = os.path.join(builddir, "conf/toaster-bblayers.conf")
329 bblayers.write('# line added by toaster build control\n' 352 with open(bblconfpath, 'w') as bblayers:
330 'BBLAYERS = "%s"' % ' '.join(layers)) 353 bblayers.write('# line added by toaster build control\n'
331 354 'BBLAYERS = "%s"' % ' '.join(layers))
332 # write configuration file 355
333 confpath = os.path.join(builddir, 'conf/toaster.conf') 356 # write configuration file
334 with open(confpath, 'w') as conf: 357 confpath = os.path.join(builddir, 'conf/toaster.conf')
335 for var in variables: 358 with open(confpath, 'w') as conf:
336 conf.write('%s="%s"\n' % (var.name, var.value)) 359 for var in variables:
337 conf.write('INHERIT+="toaster buildhistory"') 360 conf.write('%s="%s"\n' % (var.name, var.value))
361 conf.write('INHERIT+="toaster buildhistory"')
362 else:
363 # Append the Toaster-specific values directly to the bblayers.conf
364 bblconfpath = os.path.join(bitbake.req.project.builddir, "conf/bblayers.conf")
365 bblconfpath_save = os.path.join(bitbake.req.project.builddir, "conf/bblayers.conf.save")
366 shutil.copyfile(bblconfpath, bblconfpath_save)
367 with open(bblconfpath) as bblayers:
368 content = bblayers.readlines()
369 do_write = True
370 was_toaster = False
371 with open(bblconfpath,'w') as bblayers:
372 for line in content:
373 #line = line.strip('\n')
374 if 'TOASTER_CONFIG_PROLOG' in line:
375 do_write = False
376 was_toaster = True
377 elif 'TOASTER_CONFIG_EPILOG' in line:
378 do_write = True
379 elif do_write:
380 bblayers.write(line)
381 if not was_toaster:
382 bblayers.write('\n')
383 bblayers.write('#=== TOASTER_CONFIG_PROLOG ===\n')
384 bblayers.write('BBLAYERS = "\\\n')
385 for layer in layers:
386 bblayers.write(' %s \\\n' % layer)
387 bblayers.write(' "\n')
388 bblayers.write('#=== TOASTER_CONFIG_EPILOG ===\n')
389 # Append the Toaster-specific values directly to the local.conf
390 bbconfpath = os.path.join(bitbake.req.project.builddir, "conf/local.conf")
391 bbconfpath_save = os.path.join(bitbake.req.project.builddir, "conf/local.conf.save")
392 shutil.copyfile(bbconfpath, bbconfpath_save)
393 with open(bbconfpath) as f:
394 content = f.readlines()
395 do_write = True
396 was_toaster = False
397 with open(bbconfpath,'w') as conf:
398 for line in content:
399 #line = line.strip('\n')
400 if 'TOASTER_CONFIG_PROLOG' in line:
401 do_write = False
402 was_toaster = True
403 elif 'TOASTER_CONFIG_EPILOG' in line:
404 do_write = True
405 elif do_write:
406 conf.write(line)
407 if not was_toaster:
408 conf.write('\n')
409 conf.write('#=== TOASTER_CONFIG_PROLOG ===\n')
410 for var in variables:
411 if (not var.name.startswith("INTERNAL_")) and (not var.name == "BBLAYERS"):
412 conf.write('%s="%s"\n' % (var.name, var.value))
413 conf.write('#=== TOASTER_CONFIG_EPILOG ===\n')
414
415 # If 'target' is just the project preparation target, then we are done
416 for target in targets:
417 if "_PROJECT_PREPARE_" == target.target:
418 logger.debug('localhostbecontroller: Project has been prepared. Done.')
419 # Update the Build Request and release the build environment
420 bitbake.req.state = BuildRequest.REQ_COMPLETED
421 bitbake.req.save()
422 self.be.lock = BuildEnvironment.LOCK_FREE
423 self.be.save()
424 # Close the project build and progress bar
425 bitbake.req.build.outcome = Build.SUCCEEDED
426 bitbake.req.build.save()
427 # Update the project status
428 bitbake.req.project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_CLONING_SUCCESS)
429 signal_runbuilds()
430 return
338 431
339 # clean the Toaster to build environment 432 # clean the Toaster to build environment
340 env_clean = 'unset BBPATH;' # clean BBPATH for <= YP-2.4.0 433 env_clean = 'unset BBPATH;' # clean BBPATH for <= YP-2.4.0
@@ -342,9 +435,14 @@ class LocalhostBEController(BuildEnvironmentController):
342 # run bitbake server from the clone 435 # run bitbake server from the clone
343 bitbake = os.path.join(self.pokydirname, 'bitbake', 'bin', 'bitbake') 436 bitbake = os.path.join(self.pokydirname, 'bitbake', 'bin', 'bitbake')
344 toasterlayers = os.path.join(builddir,"conf/toaster-bblayers.conf") 437 toasterlayers = os.path.join(builddir,"conf/toaster-bblayers.conf")
345 self._shellcmd('%s bash -c \"source %s %s; BITBAKE_UI="knotty" %s --read %s --read %s ' 438 if not is_merged_attr:
346 '--server-only -B 0.0.0.0:0\"' % (env_clean, oe_init, 439 self._shellcmd('%s bash -c \"source %s %s; BITBAKE_UI="knotty" %s --read %s --read %s '
347 builddir, bitbake, confpath, toasterlayers), self.be.sourcedir) 440 '--server-only -B 0.0.0.0:0\"' % (env_clean, oe_init,
441 builddir, bitbake, confpath, toasterlayers), self.be.sourcedir)
442 else:
443 self._shellcmd('%s bash -c \"source %s %s; BITBAKE_UI="knotty" %s '
444 '--server-only -B 0.0.0.0:0\"' % (env_clean, oe_init,
445 builddir, bitbake), self.be.sourcedir)
348 446
349 # read port number from bitbake.lock 447 # read port number from bitbake.lock
350 self.be.bbport = -1 448 self.be.bbport = -1
@@ -390,12 +488,20 @@ class LocalhostBEController(BuildEnvironmentController):
390 log = os.path.join(builddir, 'toaster_ui.log') 488 log = os.path.join(builddir, 'toaster_ui.log')
391 local_bitbake = os.path.join(os.path.dirname(os.getenv('BBBASEDIR')), 489 local_bitbake = os.path.join(os.path.dirname(os.getenv('BBBASEDIR')),
392 'bitbake') 490 'bitbake')
393 self._shellcmd(['%s bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:%s" ' 491 if not is_merged_attr:
492 self._shellcmd(['%s bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:%s" '
394 '%s %s -u toasterui --read %s --read %s --token="" >>%s 2>&1;' 493 '%s %s -u toasterui --read %s --read %s --token="" >>%s 2>&1;'
395 'BITBAKE_UI="knotty" BBSERVER=0.0.0.0:%s %s -m)&\"' \ 494 'BITBAKE_UI="knotty" BBSERVER=0.0.0.0:%s %s -m)&\"' \
396 % (env_clean, brbe, self.be.bbport, local_bitbake, bbtargets, confpath, toasterlayers, log, 495 % (env_clean, brbe, self.be.bbport, local_bitbake, bbtargets, confpath, toasterlayers, log,
397 self.be.bbport, bitbake,)], 496 self.be.bbport, bitbake,)],
398 builddir, nowait=True) 497 builddir, nowait=True)
498 else:
499 self._shellcmd(['%s bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:%s" '
500 '%s %s -u toasterui --token="" >>%s 2>&1;'
501 'BITBAKE_UI="knotty" BBSERVER=0.0.0.0:%s %s -m)&\"' \
502 % (env_clean, brbe, self.be.bbport, local_bitbake, bbtargets, log,
503 self.be.bbport, bitbake,)],
504 builddir, nowait=True)
399 505
400 logger.debug('localhostbecontroller: Build launched, exiting. ' 506 logger.debug('localhostbecontroller: Build launched, exiting. '
401 'Follow build logs at %s' % log) 507 'Follow build logs at %s' % log)
diff --git a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py
index 791e53eabf..6a55dd46c8 100644
--- a/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py
+++ b/bitbake/lib/toaster/bldcontrol/management/commands/runbuilds.py
@@ -49,7 +49,7 @@ class Command(BaseCommand):
49 # we could not find a BEC; postpone the BR 49 # we could not find a BEC; postpone the BR
50 br.state = BuildRequest.REQ_QUEUED 50 br.state = BuildRequest.REQ_QUEUED
51 br.save() 51 br.save()
52 logger.debug("runbuilds: No build env") 52 logger.debug("runbuilds: No build env (%s)" % e)
53 return 53 return
54 54
55 logger.info("runbuilds: starting build %s, environment %s" % 55 logger.info("runbuilds: starting build %s, environment %s" %
diff --git a/bitbake/lib/toaster/orm/migrations/0018_project_specific.py b/bitbake/lib/toaster/orm/migrations/0018_project_specific.py
new file mode 100644
index 0000000000..084ecad7ba
--- /dev/null
+++ b/bitbake/lib/toaster/orm/migrations/0018_project_specific.py
@@ -0,0 +1,28 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import migrations, models
5
6class Migration(migrations.Migration):
7
8 dependencies = [
9 ('orm', '0017_distro_clone'),
10 ]
11
12 operations = [
13 migrations.AddField(
14 model_name='Project',
15 name='builddir',
16 field=models.TextField(),
17 ),
18 migrations.AddField(
19 model_name='Project',
20 name='merged_attr',
21 field=models.BooleanField(default=False)
22 ),
23 migrations.AddField(
24 model_name='Build',
25 name='progress_item',
26 field=models.CharField(max_length=40)
27 ),
28 ]
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index 3a7dff8ca6..306c4fafa8 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -121,8 +121,15 @@ class ToasterSetting(models.Model):
121 121
122 122
123class ProjectManager(models.Manager): 123class ProjectManager(models.Manager):
124 def create_project(self, name, release): 124 def create_project(self, name, release, existing_project=None):
125 if release is not None: 125 if existing_project and (release is not None):
126 prj = existing_project
127 prj.bitbake_version = release.bitbake_version
128 prj.release = release
129 # Delete the previous ProjectLayer mappings
130 for pl in ProjectLayer.objects.filter(project=prj):
131 pl.delete()
132 elif release is not None:
126 prj = self.model(name=name, 133 prj = self.model(name=name,
127 bitbake_version=release.bitbake_version, 134 bitbake_version=release.bitbake_version,
128 release=release) 135 release=release)
@@ -130,15 +137,14 @@ class ProjectManager(models.Manager):
130 prj = self.model(name=name, 137 prj = self.model(name=name,
131 bitbake_version=None, 138 bitbake_version=None,
132 release=None) 139 release=None)
133
134 prj.save() 140 prj.save()
135 141
136 for defaultconf in ToasterSetting.objects.filter( 142 for defaultconf in ToasterSetting.objects.filter(
137 name__startswith="DEFCONF_"): 143 name__startswith="DEFCONF_"):
138 name = defaultconf.name[8:] 144 name = defaultconf.name[8:]
139 ProjectVariable.objects.create(project=prj, 145 pv,create = ProjectVariable.objects.get_or_create(project=prj,name=name)
140 name=name, 146 pv.value = defaultconf.value
141 value=defaultconf.value) 147 pv.save()
142 148
143 if release is None: 149 if release is None:
144 return prj 150 return prj
@@ -197,6 +203,11 @@ class Project(models.Model):
197 user_id = models.IntegerField(null=True) 203 user_id = models.IntegerField(null=True)
198 objects = ProjectManager() 204 objects = ProjectManager()
199 205
206 # build directory override (e.g. imported)
207 builddir = models.TextField()
208 # merge the Toaster configure attributes directly into the standard conf files
209 merged_attr = models.BooleanField(default=False)
210
200 # set to True for the project which is the default container 211 # set to True for the project which is the default container
201 # for builds initiated by the command line etc. 212 # for builds initiated by the command line etc.
202 is_default= models.BooleanField(default=False) 213 is_default= models.BooleanField(default=False)
@@ -305,6 +316,15 @@ class Project(models.Model):
305 return layer_versions 316 return layer_versions
306 317
307 318
319 def get_default_image_recipe(self):
320 try:
321 return self.projectvariable_set.get(name="DEFAULT_IMAGE").value
322 except (ProjectVariable.DoesNotExist,IndexError):
323 return None;
324
325 def get_is_new(self):
326 return self.get_variable(Project.PROJECT_SPECIFIC_ISNEW)
327
308 def get_available_machines(self): 328 def get_available_machines(self):
309 """ Returns QuerySet of all Machines which are provided by the 329 """ Returns QuerySet of all Machines which are provided by the
310 Layers currently added to the Project """ 330 Layers currently added to the Project """
@@ -353,6 +373,32 @@ class Project(models.Model):
353 373
354 return queryset 374 return queryset
355 375
376 # Project Specific status management
377 PROJECT_SPECIFIC_STATUS = 'INTERNAL_PROJECT_SPECIFIC_STATUS'
378 PROJECT_SPECIFIC_CALLBACK = 'INTERNAL_PROJECT_SPECIFIC_CALLBACK'
379 PROJECT_SPECIFIC_ISNEW = 'INTERNAL_PROJECT_SPECIFIC_ISNEW'
380 PROJECT_SPECIFIC_DEFAULTIMAGE = 'PROJECT_SPECIFIC_DEFAULTIMAGE'
381 PROJECT_SPECIFIC_NONE = ''
382 PROJECT_SPECIFIC_NEW = '1'
383 PROJECT_SPECIFIC_EDIT = '2'
384 PROJECT_SPECIFIC_CLONING = '3'
385 PROJECT_SPECIFIC_CLONING_SUCCESS = '4'
386 PROJECT_SPECIFIC_CLONING_FAIL = '5'
387
388 def get_variable(self,variable,default_value = ''):
389 try:
390 return self.projectvariable_set.get(name=variable).value
391 except (ProjectVariable.DoesNotExist,IndexError):
392 return default_value
393
394 def set_variable(self,variable,value):
395 pv,create = ProjectVariable.objects.get_or_create(project = self, name = variable)
396 pv.value = value
397 pv.save()
398
399 def get_default_image(self):
400 return self.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
401
356 def schedule_build(self): 402 def schedule_build(self):
357 403
358 from bldcontrol.models import BuildRequest, BRTarget, BRLayer 404 from bldcontrol.models import BuildRequest, BRTarget, BRLayer
@@ -459,6 +505,9 @@ class Build(models.Model):
459 # number of repos cloned so far for this build (default off) 505 # number of repos cloned so far for this build (default off)
460 repos_cloned = models.IntegerField(default=1) 506 repos_cloned = models.IntegerField(default=1)
461 507
508 # Hint on current progress item
509 progress_item = models.CharField(max_length=40)
510
462 @staticmethod 511 @staticmethod
463 def get_recent(project=None): 512 def get_recent(project=None):
464 """ 513 """
diff --git a/bitbake/lib/toaster/toastergui/api.py b/bitbake/lib/toaster/toastergui/api.py
index ab6ba69e0e..1bec56d468 100644
--- a/bitbake/lib/toaster/toastergui/api.py
+++ b/bitbake/lib/toaster/toastergui/api.py
@@ -22,7 +22,9 @@ import os
22import re 22import re
23import logging 23import logging
24import json 24import json
25import subprocess
25from collections import Counter 26from collections import Counter
27from shutil import copyfile
26 28
27from orm.models import Project, ProjectTarget, Build, Layer_Version 29from orm.models import Project, ProjectTarget, Build, Layer_Version
28from orm.models import LayerVersionDependency, LayerSource, ProjectLayer 30from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
@@ -38,6 +40,18 @@ from django.core.urlresolvers import reverse
38from django.db.models import Q, F 40from django.db.models import Q, F
39from django.db import Error 41from django.db import Error
40from toastergui.templatetags.projecttags import filtered_filesizeformat 42from toastergui.templatetags.projecttags import filtered_filesizeformat
43from django.utils import timezone
44import pytz
45
46# development/debugging support
47verbose = 2
48def _log(msg):
49 if 1 == verbose:
50 print(msg)
51 elif 2 == verbose:
52 f1=open('/tmp/toaster.log', 'a')
53 f1.write("|" + msg + "|\n" )
54 f1.close()
41 55
42logger = logging.getLogger("toaster") 56logger = logging.getLogger("toaster")
43 57
@@ -137,6 +151,130 @@ class XhrBuildRequest(View):
137 return response 151 return response
138 152
139 153
154class XhrProjectUpdate(View):
155
156 def get(self, request, *args, **kwargs):
157 return HttpResponse()
158
159 def post(self, request, *args, **kwargs):
160 """
161 Project Update
162
163 Entry point: /xhr_projectupdate/<project_id>
164 Method: POST
165
166 Args:
167 pid: pid of project to update
168
169 Returns:
170 {"error": "ok"}
171 or
172 {"error": <error message>}
173 """
174
175 project = Project.objects.get(pk=kwargs['pid'])
176 logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
177
178 if 'do_update' in request.POST:
179
180 # Extract any default image recipe
181 if 'default_image' in request.POST:
182 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image']))
183 else:
184 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'')
185
186 logger.debug("ProjectUpdateCallback:Chain to the build request")
187
188 # Chain to the build request
189 xhrBuildRequest = XhrBuildRequest()
190 return xhrBuildRequest.post(request, *args, **kwargs)
191
192 logger.warning("ERROR:XhrProjectUpdate")
193 response = HttpResponse()
194 response.status_code = 500
195 return response
196
197class XhrSetDefaultImageUrl(View):
198
199 def get(self, request, *args, **kwargs):
200 return HttpResponse()
201
202 def post(self, request, *args, **kwargs):
203 """
204 Project Update
205
206 Entry point: /xhr_setdefaultimage/<project_id>
207 Method: POST
208
209 Args:
210 pid: pid of project to update default image
211
212 Returns:
213 {"error": "ok"}
214 or
215 {"error": <error message>}
216 """
217
218 project = Project.objects.get(pk=kwargs['pid'])
219 logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk))
220
221 # set any default image recipe
222 if 'targets' in request.POST:
223 default_target = str(request.POST['targets'])
224 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target)
225 logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
226 return error_response('ok')
227
228 logger.warning("ERROR:XhrSetDefaultImageUrl")
229 response = HttpResponse()
230 response.status_code = 500
231 return response
232
233
234#
235# Layer Management
236#
237# Rules for 'local_source_dir' layers
238# * Layers must have a unique name in the Layers table
239# * A 'local_source_dir' layer is supposed to be shared
240# by all projects that use it, so that it can have the
241# same logical name
242# * Each project that uses a layer will have its own
243# LayerVersion and Project Layer for it
244# * During the Paroject delete process, when the last
245# LayerVersion for a 'local_source_dir' layer is deleted
246# then the Layer record is deleted to remove orphans
247#
248
249def scan_layer_content(layer,layer_version):
250 # if this is a local layer directory, we can immediately scan its content
251 if layer.local_source_dir:
252 try:
253 # recipes-*/*/*.bb
254 cmd = '%s %s' % ('ls', os.path.join(layer.local_source_dir,'recipes-*/*/*.bb'))
255 recipes_list = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()
256 recipes_list = recipes_list.decode("utf-8").strip()
257 if recipes_list and 'No such' not in recipes_list:
258 for recipe in recipes_list.split('\n'):
259 recipe_path = recipe[recipe.rfind('recipes-'):]
260 recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','')
261 recipe_ver = recipe_name.rfind('_')
262 if recipe_ver > 0:
263 recipe_name = recipe_name[0:recipe_ver]
264 if recipe_name:
265 ro, created = Recipe.objects.get_or_create(
266 layer_version=layer_version,
267 name=recipe_name
268 )
269 if created:
270 ro.file_path = recipe_path
271 ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name)
272 ro.description = ro.summary
273 ro.save()
274
275 except Exception as e:
276 logger.warning("ERROR:scan_layer_content: %s" % e)
277
140class XhrLayer(View): 278class XhrLayer(View):
141 """ Delete, Get, Add and Update Layer information 279 """ Delete, Get, Add and Update Layer information
142 280
@@ -265,6 +403,7 @@ class XhrLayer(View):
265 (csv)] 403 (csv)]
266 404
267 """ 405 """
406
268 try: 407 try:
269 project = Project.objects.get(pk=kwargs['pid']) 408 project = Project.objects.get(pk=kwargs['pid'])
270 409
@@ -285,7 +424,13 @@ class XhrLayer(View):
285 if layer_data['name'] in existing_layers: 424 if layer_data['name'] in existing_layers:
286 return JsonResponse({"error": "layer-name-exists"}) 425 return JsonResponse({"error": "layer-name-exists"})
287 426
288 layer = Layer.objects.create(name=layer_data['name']) 427 if ('local_source_dir' in layer_data):
428 # Local layer can be shared across projects. They have no 'release'
429 # and are not included in get_all_compatible_layer_versions() above
430 layer,created = Layer.objects.get_or_create(name=layer_data['name'])
431 _log("Local Layer created=%s" % created)
432 else:
433 layer = Layer.objects.create(name=layer_data['name'])
289 434
290 layer_version = Layer_Version.objects.create( 435 layer_version = Layer_Version.objects.create(
291 layer=layer, 436 layer=layer,
@@ -293,7 +438,7 @@ class XhrLayer(View):
293 layer_source=LayerSource.TYPE_IMPORTED) 438 layer_source=LayerSource.TYPE_IMPORTED)
294 439
295 # Local layer 440 # Local layer
296 if ('local_source_dir' in layer_data) and layer.local_source_dir: 441 if ('local_source_dir' in layer_data): ### and layer.local_source_dir:
297 layer.local_source_dir = layer_data['local_source_dir'] 442 layer.local_source_dir = layer_data['local_source_dir']
298 # git layer 443 # git layer
299 elif 'vcs_url' in layer_data: 444 elif 'vcs_url' in layer_data:
@@ -325,6 +470,9 @@ class XhrLayer(View):
325 'layerdetailurl': 470 'layerdetailurl':
326 layer_dep.get_detailspage_url(project.pk)}) 471 layer_dep.get_detailspage_url(project.pk)})
327 472
473 # Scan the layer's content and update components
474 scan_layer_content(layer,layer_version)
475
328 except Layer_Version.DoesNotExist: 476 except Layer_Version.DoesNotExist:
329 return error_response("layer-dep-not-found") 477 return error_response("layer-dep-not-found")
330 except Project.DoesNotExist: 478 except Project.DoesNotExist:
@@ -1014,8 +1162,24 @@ class XhrProject(View):
1014 state=BuildRequest.REQ_INPROGRESS): 1162 state=BuildRequest.REQ_INPROGRESS):
1015 XhrBuildRequest.cancel_build(br) 1163 XhrBuildRequest.cancel_build(br)
1016 1164
1165 # gather potential orphaned local layers attached to this project
1166 project_local_layer_list = []
1167 for pl in ProjectLayer.objects.filter(project=project):
1168 if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED:
1169 project_local_layer_list.append(pl.layercommit.layer)
1170
1171 # deep delete the project and its dependencies
1017 project.delete() 1172 project.delete()
1018 1173
1174 # delete any local layers now orphaned
1175 _log("LAYER_ORPHAN_CHECK:Check for orphaned layers")
1176 for layer in project_local_layer_list:
1177 layer_refs = Layer_Version.objects.filter(layer=layer)
1178 _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs)))
1179 if 0 == len(layer_refs):
1180 _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name))
1181 Layer.objects.filter(pk=layer.id).delete()
1182
1019 except Project.DoesNotExist: 1183 except Project.DoesNotExist:
1020 return error_response("Project %s does not exist" % 1184 return error_response("Project %s does not exist" %
1021 kwargs['project_id']) 1185 kwargs['project_id'])
diff --git a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
index 9f9eda1e1e..a5a6563d1a 100644
--- a/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
+++ b/bitbake/lib/toaster/toastergui/static/js/layerBtn.js
@@ -67,6 +67,18 @@ function layerBtnsInit() {
67 }); 67 });
68 }); 68 });
69 69
70 $("td .set-default-recipe-btn").unbind('click');
71 $("td .set-default-recipe-btn").click(function(e){
72 e.preventDefault();
73 var recipe = $(this).data('recipe-name');
74
75 libtoaster.setDefaultImage(null, recipe,
76 function(){
77 /* Success */
78 window.location.replace(libtoaster.ctx.projectSpecificPageUrl);
79 });
80 });
81
70 82
71 $(".customise-btn").unbind('click'); 83 $(".customise-btn").unbind('click');
72 $(".customise-btn").click(function(e){ 84 $(".customise-btn").click(function(e){
diff --git a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
index 6f9b5d0f00..2e8863af26 100644
--- a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
+++ b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
@@ -465,6 +465,108 @@ var libtoaster = (function () {
465 $.cookie('toaster-notification', JSON.stringify(data), { path: '/'}); 465 $.cookie('toaster-notification', JSON.stringify(data), { path: '/'});
466 } 466 }
467 467
468 /* _updateProject:
469 * url: xhrProjectUpdateUrl or null for current project
470 * onsuccess: callback for successful execution
471 * onfail: callback for failed execution
472 */
473 function _updateProject (url, targets, default_image, onsuccess, onfail) {
474
475 if (!url)
476 url = libtoaster.ctx.xhrProjectUpdateUrl;
477
478 /* Flatten the array of targets into a space spearated list */
479 if (targets instanceof Array){
480 targets = targets.reduce(function(prevV, nextV){
481 return prev + ' ' + next;
482 });
483 }
484
485 $.ajax( {
486 type: "POST",
487 url: url,
488 data: { 'do_update' : 'True' , 'targets' : targets , 'default_image' : default_image , },
489 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
490 success: function (_data) {
491 if (_data.error !== "ok") {
492 console.warn(_data.error);
493 } else {
494 if (onsuccess !== undefined) onsuccess(_data);
495 }
496 },
497 error: function (_data) {
498 console.warn("Call failed");
499 console.warn(_data);
500 if (onfail) onfail(data);
501 } });
502 }
503
504 /* _cancelProject:
505 * url: xhrProjectUpdateUrl or null for current project
506 * onsuccess: callback for successful execution
507 * onfail: callback for failed execution
508 */
509 function _cancelProject (url, onsuccess, onfail) {
510
511 if (!url)
512 url = libtoaster.ctx.xhrProjectCancelUrl;
513
514 $.ajax( {
515 type: "POST",
516 url: url,
517 data: { 'do_cancel' : 'True' },
518 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
519 success: function (_data) {
520 if (_data.error !== "ok") {
521 console.warn(_data.error);
522 } else {
523 if (onsuccess !== undefined) onsuccess(_data);
524 }
525 },
526 error: function (_data) {
527 console.warn("Call failed");
528 console.warn(_data);
529 if (onfail) onfail(data);
530 } });
531 }
532
533 /* _setDefaultImage:
534 * url: xhrSetDefaultImageUrl or null for current project
535 * targets: an array or space separated list of targets to set as default
536 * onsuccess: callback for successful execution
537 * onfail: callback for failed execution
538 */
539 function _setDefaultImage (url, targets, onsuccess, onfail) {
540
541 if (!url)
542 url = libtoaster.ctx.xhrSetDefaultImageUrl;
543
544 /* Flatten the array of targets into a space spearated list */
545 if (targets instanceof Array){
546 targets = targets.reduce(function(prevV, nextV){
547 return prev + ' ' + next;
548 });
549 }
550
551 $.ajax( {
552 type: "POST",
553 url: url,
554 data: { 'targets' : targets },
555 headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
556 success: function (_data) {
557 if (_data.error !== "ok") {
558 console.warn(_data.error);
559 } else {
560 if (onsuccess !== undefined) onsuccess(_data);
561 }
562 },
563 error: function (_data) {
564 console.warn("Call failed");
565 console.warn(_data);
566 if (onfail) onfail(data);
567 } });
568 }
569
468 return { 570 return {
469 enableAjaxLoadingTimer: _enableAjaxLoadingTimer, 571 enableAjaxLoadingTimer: _enableAjaxLoadingTimer,
470 disableAjaxLoadingTimer: _disableAjaxLoadingTimer, 572 disableAjaxLoadingTimer: _disableAjaxLoadingTimer,
@@ -485,6 +587,9 @@ var libtoaster = (function () {
485 createCustomRecipe: _createCustomRecipe, 587 createCustomRecipe: _createCustomRecipe,
486 makeProjectNameValidation: _makeProjectNameValidation, 588 makeProjectNameValidation: _makeProjectNameValidation,
487 setNotification: _setNotification, 589 setNotification: _setNotification,
590 updateProject : _updateProject,
591 cancelProject : _cancelProject,
592 setDefaultImage : _setDefaultImage,
488 }; 593 };
489})(); 594})();
490 595
diff --git a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
index c0c5fa9589..f07ccf8181 100644
--- a/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
+++ b/bitbake/lib/toaster/toastergui/static/js/mrbsection.js
@@ -86,7 +86,7 @@ function mrbSectionInit(ctx){
86 if (buildFinished(build)) { 86 if (buildFinished(build)) {
87 // a build finished: reload the whole page so that the build 87 // a build finished: reload the whole page so that the build
88 // shows up in the builds table 88 // shows up in the builds table
89 window.location.reload(); 89 window.location.reload(true);
90 } 90 }
91 else if (stateChanged(build)) { 91 else if (stateChanged(build)) {
92 // update the whole template 92 // update the whole template
@@ -110,6 +110,8 @@ function mrbSectionInit(ctx){
110 // update the clone progress text 110 // update the clone progress text
111 selector = '#repos-cloned-percentage-' + build.id; 111 selector = '#repos-cloned-percentage-' + build.id;
112 $(selector).html(build.repos_cloned_percentage); 112 $(selector).html(build.repos_cloned_percentage);
113 selector = '#repos-cloned-progressitem-' + build.id;
114 $(selector).html('('+build.progress_item+')');
113 115
114 // update the recipe progress bar 116 // update the recipe progress bar
115 selector = '#repos-cloned-percentage-bar-' + build.id; 117 selector = '#repos-cloned-percentage-bar-' + build.id;
diff --git a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
index 69220aaf57..3f9e186708 100644
--- a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
+++ b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
@@ -14,6 +14,9 @@ function projectTopBarInit(ctx) {
14 var newBuildTargetBuildBtn = $("#build-button"); 14 var newBuildTargetBuildBtn = $("#build-button");
15 var selectedTarget; 15 var selectedTarget;
16 16
17 var updateProjectBtn = $("#update-project-button");
18 var cancelProjectBtn = $("#cancel-project-button");
19
17 /* Project name change functionality */ 20 /* Project name change functionality */
18 projectNameFormToggle.click(function(e){ 21 projectNameFormToggle.click(function(e){
19 e.preventDefault(); 22 e.preventDefault();
@@ -89,6 +92,25 @@ function projectTopBarInit(ctx) {
89 }, null); 92 }, null);
90 }); 93 });
91 94
95 updateProjectBtn.click(function (e) {
96 e.preventDefault();
97
98 selectedTarget = { name: "_PROJECT_PREPARE_" };
99
100 /* Save current default build image, fire off the build */
101 libtoaster.updateProject(null, selectedTarget.name, newBuildTargetInput.val().trim(),
102 function(){
103 window.location.replace(libtoaster.ctx.projectSpecificPageUrl);
104 }, null);
105 });
106
107 cancelProjectBtn.click(function (e) {
108 e.preventDefault();
109
110 /* redirect to 'done/canceled' landing page */
111 window.location.replace(libtoaster.ctx.landingSpecificCancelURL);
112 });
113
92 /* Call makeProjectNameValidation function */ 114 /* Call makeProjectNameValidation function */
93 libtoaster.makeProjectNameValidation($("#project-name-change-input"), 115 libtoaster.makeProjectNameValidation($("#project-name-change-input"),
94 $("#hint-error-project-name"), $("#validate-project-name"), 116 $("#hint-error-project-name"), $("#validate-project-name"),
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index dca2fa2913..03bd2ae9c6 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -35,6 +35,8 @@ from toastergui.tablefilter import TableFilterActionToggle
35from toastergui.tablefilter import TableFilterActionDateRange 35from toastergui.tablefilter import TableFilterActionDateRange
36from toastergui.tablefilter import TableFilterActionDay 36from toastergui.tablefilter import TableFilterActionDay
37 37
38import os
39
38class ProjectFilters(object): 40class ProjectFilters(object):
39 @staticmethod 41 @staticmethod
40 def in_project(project_layers): 42 def in_project(project_layers):
@@ -339,6 +341,8 @@ class RecipesTable(ToasterTable):
339 'filter_name' : "in_current_project", 341 'filter_name' : "in_current_project",
340 'static_data_name' : "add-del-layers", 342 'static_data_name' : "add-del-layers",
341 'static_data_template' : '{% include "recipe_btn.html" %}'} 343 'static_data_template' : '{% include "recipe_btn.html" %}'}
344 if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
345 build_col['static_data_template'] = '{% include "recipe_add_btn.html" %}'
342 346
343 def get_context_data(self, **kwargs): 347 def get_context_data(self, **kwargs):
344 project = Project.objects.get(pk=kwargs['pid']) 348 project = Project.objects.get(pk=kwargs['pid'])
diff --git a/bitbake/lib/toaster/toastergui/templates/base_specific.html b/bitbake/lib/toaster/toastergui/templates/base_specific.html
new file mode 100644
index 0000000000..e377cadd73
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/base_specific.html
@@ -0,0 +1,128 @@
1<!DOCTYPE html>
2{% load static %}
3{% load projecttags %}
4{% load project_url_tag %}
5<html lang="en">
6 <head>
7 <title>
8 {% block title %} Toaster {% endblock %}
9 </title>
10 <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" type="text/css"/>
11 <!--link rel="stylesheet" href="{% static 'css/bootstrap-theme.css' %}" type="text/css"/-->
12 <link rel="stylesheet" href="{% static 'css/font-awesome.min.css' %}" type='text/css'/>
13 <link rel="stylesheet" href="{% static 'css/default.css' %}" type='text/css'/>
14
15 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
16 <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
17 <script src="{% static 'js/jquery-2.0.3.min.js' %}">
18 </script>
19 <script src="{% static 'js/jquery.cookie.js' %}">
20 </script>
21 <script src="{% static 'js/bootstrap.min.js' %}">
22 </script>
23 <script src="{% static 'js/typeahead.jquery.js' %}">
24 </script>
25 <script src="{% static 'js/jsrender.min.js' %}">
26 </script>
27 <script src="{% static 'js/highlight.pack.js' %}">
28 </script>
29 <script src="{% static 'js/libtoaster.js' %}">
30 </script>
31 {% if DEBUG %}
32 <script>
33 libtoaster.debug = true;
34 </script>
35 {% endif %}
36 <script>
37 /* Set JsRender delimiters (mrb_section.html) different than Django's */
38 $.views.settings.delimiters("<%", "%>");
39
40 /* This table allows Django substitutions to be passed to libtoaster.js */
41 libtoaster.ctx = {
42 jsUrl : "{% static 'js/' %}",
43 htmlUrl : "{% static 'html/' %}",
44 projectsUrl : "{% url 'all-projects' %}",
45 projectsTypeAheadUrl: {% url 'xhr_projectstypeahead' as prjurl%}{{prjurl|json}},
46 {% if project.id %}
47 landingSpecificURL : "{% url 'landing_specific' project.id %}",
48 landingSpecificCancelURL : "{% url 'landing_specific_cancel' project.id %}",
49 projectId : {{project.id}},
50 projectPageUrl : {% url 'project' project.id as purl %}{{purl|json}},
51 projectSpecificPageUrl : {% url 'project_specific' project.id as purl %}{{purl|json}},
52 xhrProjectUrl : {% url 'xhr_project' project.id as pxurl %}{{pxurl|json}},
53 projectName : {{project.name|json}},
54 recipesTypeAheadUrl: {% url 'xhr_recipestypeahead' project.id as paturl%}{{paturl|json}},
55 layersTypeAheadUrl: {% url 'xhr_layerstypeahead' project.id as paturl%}{{paturl|json}},
56 machinesTypeAheadUrl: {% url 'xhr_machinestypeahead' project.id as paturl%}{{paturl|json}},
57 distrosTypeAheadUrl: {% url 'xhr_distrostypeahead' project.id as paturl%}{{paturl|json}},
58 projectBuildsUrl: {% url 'projectbuilds' project.id as pburl %}{{pburl|json}},
59 xhrCustomRecipeUrl : "{% url 'xhr_customrecipe' %}",
60 projectId : {{project.id}},
61 xhrBuildRequestUrl: "{% url 'xhr_buildrequest' project.id %}",
62 mostRecentBuildsUrl: "{% url 'most_recent_builds' %}?project_id={{project.id}}",
63 xhrProjectUpdateUrl: "{% url 'xhr_projectupdate' project.id %}",
64 xhrProjectCancelUrl: "{% url 'landing_specific_cancel' project.id %}",
65 xhrSetDefaultImageUrl: "{% url 'xhr_setdefaultimage' project.id %}",
66 {% else %}
67 mostRecentBuildsUrl: "{% url 'most_recent_builds' %}",
68 projectId : undefined,
69 projectPageUrl : undefined,
70 projectName : undefined,
71 {% endif %}
72 };
73 </script>
74 {% block extraheadcontent %}
75 {% endblock %}
76 </head>
77
78 <body>
79
80 {% csrf_token %}
81 <div id="loading-notification" class="alert alert-warning lead text-center" style="display:none">
82 Loading <i class="fa-pulse icon-spinner"></i>
83 </div>
84
85 <div id="change-notification" class="alert alert-info alert-dismissible change-notification" style="display:none">
86 <button type="button" class="close" id="hide-alert" data-toggle="alert">&times;</button>
87 <span id="change-notification-msg"></span>
88 </div>
89
90 <nav class="navbar navbar-default navbar-fixed-top">
91 <div class="container-fluid">
92 <div class="navbar-header">
93 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#global-nav" aria-expanded="false">
94 <span class="sr-only">Toggle navigation</span>
95 <span class="icon-bar"></span>
96 <span class="icon-bar"></span>
97 <span class="icon-bar"></span>
98 </button>
99 <div class="toaster-navbar-brand">
100 {% if project_specific %}
101 <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/>
102 Toaster
103 {% else %}
104 <a href="/">
105 </a>
106 <a href="/">
107 <img class="logo" src="{% static 'img/logo.png' %}" class="" alt="Yocto Project logo"/>
108 </a>
109 <a class="brand" href="/">Toaster</a>
110 {% endif %}
111 {% if DEBUG %}
112 <span class="glyphicon glyphicon-info-sign" title="<strong>Toaster version information</strong>" data-content="<dl><dt>Git branch</dt><dd>{{TOASTER_BRANCH}}</dd><dt>Git revision</dt><dd>{{TOASTER_REVISION}}</dd></dl>"></i>
113 {% endif %}
114 </div>
115 </div>
116 <div class="collapse navbar-collapse" id="global-nav">
117 <ul class="nav navbar-nav">
118 <h3> Project Configuration Page </h3>
119 </div>
120 </div>
121 </nav>
122
123 <div class="container-fluid">
124 {% block pagecontent %}
125 {% endblock %}
126 </div>
127 </body>
128</html>
diff --git a/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html b/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html
new file mode 100644
index 0000000000..d0b588de98
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/baseprojectspecificpage.html
@@ -0,0 +1,48 @@
1{% extends "base_specific.html" %}
2
3{% load projecttags %}
4{% load humanize %}
5
6{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %}
7
8{% block pagecontent %}
9
10<div class="row">
11 {% include "project_specific_topbar.html" %}
12 <script type="text/javascript">
13$(document).ready(function(){
14 $("#config-nav .nav li a").each(function(){
15 if (window.location.pathname === $(this).attr('href'))
16 $(this).parent().addClass('active');
17 else
18 $(this).parent().removeClass('active');
19 });
20
21 $("#topbar-configuration-tab").addClass("active")
22 });
23 </script>
24
25 <!-- only on config pages -->
26 <div id="config-nav" class="col-md-2">
27 <ul class="nav nav-pills nav-stacked">
28 <li><a class="nav-parent" href="{% url 'project' project.id %}">Configuration</a></li>
29 <li class="nav-header">Compatible metadata</li>
30 <li><a href="{% url 'projectcustomimages' project.id %}">Custom images</a></li>
31 <li><a href="{% url 'projectimagerecipes' project.id %}">Image recipes</a></li>
32 <li><a href="{% url 'projectsoftwarerecipes' project.id %}">Software recipes</a></li>
33 <li><a href="{% url 'projectmachines' project.id %}">Machines</a></li>
34 <li><a href="{% url 'projectlayers' project.id %}">Layers</a></li>
35 <li><a href="{% url 'projectdistros' project.id %}">Distros</a></li>
36 <li class="nav-header">Extra configuration</li>
37 <li><a href="{% url 'projectconf' project.id %}">BitBake variables</a></li>
38
39 <li class="nav-header">Actions</li>
40 </ul>
41 </div>
42 <div class="col-md-10">
43 {% block projectinfomain %}{% endblock %}
44 </div>
45
46</div>
47{% endblock %}
48
diff --git a/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html b/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
index b3eabe1a26..99fbb38970 100644
--- a/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
+++ b/bitbake/lib/toaster/toastergui/templates/generic-toastertable-page.html
@@ -1,4 +1,4 @@
1{% extends "baseprojectpage.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
diff --git a/bitbake/lib/toaster/toastergui/templates/importlayer.html b/bitbake/lib/toaster/toastergui/templates/importlayer.html
index 97d52c76c1..e0c987eef1 100644
--- a/bitbake/lib/toaster/toastergui/templates/importlayer.html
+++ b/bitbake/lib/toaster/toastergui/templates/importlayer.html
@@ -1,4 +1,4 @@
1{% extends "base.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
@@ -6,7 +6,7 @@
6{% block pagecontent %} 6{% block pagecontent %}
7 7
8<div class="row"> 8<div class="row">
9 {% include "projecttopbar.html" %} 9 {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %}
10 {% if project and project.release %} 10 {% if project and project.release %}
11 <script src="{% static 'js/layerDepsModal.js' %}"></script> 11 <script src="{% static 'js/layerDepsModal.js' %}"></script>
12 <script src="{% static 'js/importlayer.js' %}"></script> 12 <script src="{% static 'js/importlayer.js' %}"></script>
diff --git a/bitbake/lib/toaster/toastergui/templates/landing_specific.html b/bitbake/lib/toaster/toastergui/templates/landing_specific.html
new file mode 100644
index 0000000000..e289c7d4a5
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/landing_specific.html
@@ -0,0 +1,50 @@
1{% extends "base_specific.html" %}
2
3{% load static %}
4{% load projecttags %}
5{% load humanize %}
6
7{% block title %} Welcome to Toaster {% endblock %}
8
9{% block pagecontent %}
10
11 <div class="container">
12 <div class="row">
13 <!-- Empty - no build module -->
14 <div class="page-header top-air">
15 <h1>
16 Configuration {% if status == "cancel" %}Canceled{% else %}Completed{% endif %}! You can now close this window.
17 </h1>
18 </div>
19 <div class="alert alert-info lead">
20 <p>
21 Your project configuration {% if status == "cancel" %}changes have been canceled{% else %}has completed!{% endif %}
22 <br>
23 <br>
24 <ul>
25 <li>
26 The Toaster instance for project configuration has been shut down
27 </li>
28 <li>
29 You can start Toaster independently for advanced project management and analysis:
30 <pre><code>
31 Set up bitbake environment:
32 $ cd {{install_dir}}
33 $ . oe-init-build-env [toaster_server]
34
35 Option 1: Start a local Toaster server, open local browser to "localhost:8000"
36 $ . toaster start webport=8000
37
38 Option 2: Start a shared Toaster server, open any browser to "[host_ip]:8000"
39 $ . toaster start webport=0.0.0.0:8000
40
41 To stop the Toaster server:
42 $ . toaster stop
43 </code></pre>
44 </li>
45 </ul>
46 </p>
47 </div>
48 </div>
49
50{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/layerdetails.html b/bitbake/lib/toaster/toastergui/templates/layerdetails.html
index e0069db80c..1e26e31c8b 100644
--- a/bitbake/lib/toaster/toastergui/templates/layerdetails.html
+++ b/bitbake/lib/toaster/toastergui/templates/layerdetails.html
@@ -1,4 +1,4 @@
1{% extends "base.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
@@ -310,6 +310,7 @@
310 {% endwith %} 310 {% endwith %}
311 {% endwith %} 311 {% endwith %}
312 </div> 312 </div>
313
313 </div> <!-- end tab content --> 314 </div> <!-- end tab content -->
314 </div> <!-- end tabable --> 315 </div> <!-- end tabable -->
315 316
diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index c5b9fe90d3..98d9fac822 100644
--- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -119,7 +119,7 @@
119 title="Toaster is cloning the repos required for your build"> 119 title="Toaster is cloning the repos required for your build">
120 </span> 120 </span>
121 121
122 Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete 122 Cloning <span id="repos-cloned-percentage-<%:id%>"><%:repos_cloned_percentage%></span>% complete <span id="repos-cloned-progressitem-<%:id%>">(<%:progress_item%>)</span>
123 123
124 <%include tmpl='#cancel-template'/%> 124 <%include tmpl='#cancel-template'/%>
125 </div> 125 </div>
diff --git a/bitbake/lib/toaster/toastergui/templates/newcustomimage.html b/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
index 980179a406..0766e5e4cf 100644
--- a/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
+++ b/bitbake/lib/toaster/toastergui/templates/newcustomimage.html
@@ -1,4 +1,4 @@
1{% extends "base.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,base.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4{% load static %} 4{% load static %}
@@ -8,7 +8,7 @@
8 8
9<div class="row"> 9<div class="row">
10 10
11 {% include "projecttopbar.html" %} 11 {% include project_specific|yesno:"project_specific_topbar.html,projecttopbar.html" %}
12 12
13 <div class="col-md-12"> 13 <div class="col-md-12">
14 {% url table_name project.id as xhr_table_url %} 14 {% url table_name project.id as xhr_table_url %}
diff --git a/bitbake/lib/toaster/toastergui/templates/newproject_specific.html b/bitbake/lib/toaster/toastergui/templates/newproject_specific.html
new file mode 100644
index 0000000000..cfa77f2e40
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/newproject_specific.html
@@ -0,0 +1,95 @@
1{% extends "base.html" %}
2{% load projecttags %}
3{% load humanize %}
4
5{% block title %} Create a new project - Toaster {% endblock %}
6
7{% block pagecontent %}
8<div class="row">
9 <div class="col-md-12">
10 <div class="page-header">
11 <h1>Create a new project</h1>
12 </div>
13 {% if alert %}
14 <div class="alert alert-danger" role="alert">{{alert}}</div>
15 {% endif %}
16
17 <form method="POST" action="{%url "newproject_specific" project_pk %}">{% csrf_token %}
18 <div class="form-group" id="validate-project-name">
19 <label class="control-label">Project name <span class="text-muted">(required)</span></label>
20 <input type="text" class="form-control" required id="new-project-name" name="display_projectname" value="{{projectname}}" disabled>
21 </div>
22 <p class="help-block text-danger" style="display: none;" id="hint-error-project-name">A project with this name exists. Project names must be unique.</p>
23 <input type="hidden" name="ptype" value="build" />
24 <input type="hidden" name="projectname" value="{{projectname}}" />
25
26 {% if releases.count > 0 %}
27 <div class="release form-group">
28 {% if releases.count > 1 %}
29 <label class="control-label">
30 Release
31 <span class="glyphicon glyphicon-question-sign get-help" title="The version of the build system you want to use"></span>
32 </label>
33 <select name="projectversion" id="projectversion" class="form-control">
34 {% for release in releases %}
35 <option value="{{release.id}}"
36 {%if defaultbranch == release.name %}
37 selected
38 {%endif%}
39 >{{release.description}}</option>
40 {% endfor %}
41 </select>
42 <div class="row">
43 <div class="col-md-4">
44 {% for release in releases %}
45 <div class="helptext" id="description-{{release.id}}" style="display: none">
46 <span class="help-block">{{release.helptext|safe}}</span>
47 </div>
48 {% endfor %}
49 {% else %}
50 <input type="hidden" name="projectversion" value="{{releases.0.id}}"/>
51 {% endif %}
52 </div>
53 </div>
54 </fieldset>
55 {% endif %}
56 <div class="top-air">
57 <input type="submit" id="create-project-button" class="btn btn-primary btn-lg" value="Create project"/>
58 <span class="help-inline" style="vertical-align:middle;">To create a project, you need to specify the release</span>
59 </div>
60
61 </form>
62 </div>
63 </div>
64
65 <script type="text/javascript">
66 $(document).ready(function () {
67 // hide the new project button, name is preset
68 $("#new-project-button").hide();
69
70 // enable submit button when all required fields are populated
71 $("input#new-project-name").on('input', function() {
72 if ($("input#new-project-name").val().length > 0 ){
73 $('.btn-primary').removeAttr('disabled');
74 $(".help-inline").css('visibility','hidden');
75 }
76 else {
77 $('.btn-primary').attr('disabled', 'disabled');
78 $(".help-inline").css('visibility','visible');
79 }
80 });
81
82 // show relevant help text for the selected release
83 var selected_release = $('select').val();
84 $("#description-" + selected_release).show();
85
86 $('select').change(function(){
87 var new_release = $('select').val();
88 $(".helptext").hide();
89 $('#description-' + new_release).fadeIn();
90 });
91
92 });
93 </script>
94
95{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/project.html b/bitbake/lib/toaster/toastergui/templates/project.html
index 11603d1e12..fa41e3c909 100644
--- a/bitbake/lib/toaster/toastergui/templates/project.html
+++ b/bitbake/lib/toaster/toastergui/templates/project.html
@@ -1,4 +1,4 @@
1{% extends "baseprojectpage.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
2 2
3{% load projecttags %} 3{% load projecttags %}
4{% load humanize %} 4{% load humanize %}
@@ -18,7 +18,7 @@
18 try { 18 try {
19 projectPageInit(ctx); 19 projectPageInit(ctx);
20 } catch (e) { 20 } catch (e) {
21 document.write("Sorry, An error has occurred loading this page"); 21 document.write("Sorry, An error has occurred loading this page (project):"+e);
22 console.warn(e); 22 console.warn(e);
23 } 23 }
24 }); 24 });
@@ -93,6 +93,7 @@
93 </form> 93 </form>
94 </div> 94 </div>
95 95
96 {% if not project_specific %}
96 <div class="well well-transparent"> 97 <div class="well well-transparent">
97 <h3>Most built recipes</h3> 98 <h3>Most built recipes</h3>
98 99
@@ -105,6 +106,7 @@
105 </ul> 106 </ul>
106 <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button> 107 <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button>
107 </div> 108 </div>
109 {% endif %}
108 110
109 <div class="well well-transparent"> 111 <div class="well well-transparent">
110 <h3>Project release</h3> 112 <h3>Project release</h3>
@@ -157,5 +159,6 @@
157 <ul class="list-unstyled lead" id="layers-in-project-list"> 159 <ul class="list-unstyled lead" id="layers-in-project-list">
158 </ul> 160 </ul>
159 </div> 161 </div>
162
160</div> 163</div>
161{% endblock %} 164{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/project_specific.html b/bitbake/lib/toaster/toastergui/templates/project_specific.html
new file mode 100644
index 0000000000..f625d18baf
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/project_specific.html
@@ -0,0 +1,162 @@
1{% extends "baseprojectspecificpage.html" %}
2
3{% load projecttags %}
4{% load humanize %}
5{% load static %}
6
7{% block title %} Configuration - {{project.name}} - Toaster {% endblock %}
8{% block projectinfomain %}
9
10<script src="{% static 'js/layerDepsModal.js' %}"></script>
11<script src="{% static 'js/projectpage.js' %}"></script>
12<script>
13 $(document).ready(function (){
14 var ctx = {
15 testReleaseChangeUrl: "{% url 'xhr_testreleasechange' project.id %}",
16 };
17
18 try {
19 projectPageInit(ctx);
20 } catch (e) {
21 document.write("Sorry, An error has occurred loading this page");
22 console.warn(e);
23 }
24 });
25</script>
26
27<div id="delete-project-modal" class="modal fade" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
28 <div class="modal-dialog">
29 <div class="modal-content">
30 <div class="modal-header">
31 <h4>Are you sure you want to delete this project?</h4>
32 </div>
33 <div class="modal-body">
34 <p>Deleting the <strong class="project-name"></strong> project
35 will:</p>
36 <ul>
37 <li>Cancel its builds currently in progress</li>
38 <li>Remove its configuration information</li>
39 <li>Remove its imported layers</li>
40 <li>Remove its custom images</li>
41 <li>Remove all its build information</li>
42 </ul>
43 </div>
44 <div class="modal-footer">
45 <button type="button" class="btn btn-primary" id="delete-project-confirmed">
46 <span data-role="submit-state">Delete project</span>
47 <span data-role="loading-state" style="display:none">
48 <span class="fa-pulse">
49 <i class="fa-pulse icon-spinner"></i>
50 </span>
51 &nbsp;Deleting project...
52 </span>
53 </button>
54 <button type="button" class="btn btn-link" data-dismiss="modal">Cancel</button>
55 </div>
56 </div><!-- /.modal-content -->
57 </div><!-- /.modal-dialog -->
58</div>
59
60
61<div class="row" id="project-page" style="display:none">
62 <div class="col-md-6">
63 <div class="well well-transparent" id="machine-section">
64 <h3>Machine</h3>
65
66 <p class="lead"><span id="project-machine-name"></span> <span class="glyphicon glyphicon-edit" id="change-machine-toggle"></span></p>
67
68 <form id="select-machine-form" style="display:none;" class="form-inline">
69 <span class="help-block">Machine suggestions come from the list of layers added to your project. If you don't see the machine you are looking for, <a href="{% url 'projectmachines' project.id %}">check the full list of machines</a></span>
70 <div class="form-group" id="machine-input-form">
71 <input class="form-control" id="machine-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text">
72 </div>
73 <button id="machine-change-btn" class="btn btn-default" type="button">Save</button>
74 <a href="#" id="cancel-machine-change" class="btn btn-link">Cancel</a>
75 <span class="help-block text-danger" id="invalid-machine-name-help" style="display:none">A valid machine name cannot include spaces.</span>
76 <p class="form-link"><a href="{% url 'projectmachines' project.id %}">View compatible machines</a></p>
77 </form>
78 </div>
79
80 <div class="well well-transparent" id="distro-section">
81 <h3>Distro</h3>
82
83 <p class="lead"><span id="project-distro-name"></span> <span class="glyphicon glyphicon-edit" id="change-distro-toggle"></span></p>
84
85 <form id="select-distro-form" style="display:none;" class="form-inline">
86 <span class="help-block">Distro suggestions come from the Layer Index</a></span>
87 <div class="form-group">
88 <input class="form-control" id="distro-change-input" autocomplete="off" value="" data-provide="typeahead" data-minlength="1" data-autocomplete="off" type="text">
89 </div>
90 <button id="distro-change-btn" class="btn btn-default" type="button">Save</button>
91 <a href="#" id="cancel-distro-change" class="btn btn-link">Cancel</a>
92 <p class="form-link"><a href="{% url 'projectdistros' project.id %}">View compatible distros</a></p>
93 </form>
94 </div>
95
96 <div class="well well-transparent">
97 <h3>Most built recipes</h3>
98
99 <div class="alert alert-info" style="display:none" id="no-most-built">
100 <h4>You haven't built any recipes yet</h4>
101 <p class="form-link"><a href="{% url 'projectimagerecipes' project.id %}">Choose a recipe to build</a></p>
102 </div>
103
104 <ul class="list-unstyled lead" id="freq-build-list">
105 </ul>
106 <button class="btn btn-primary" id="freq-build-btn" disabled="disabled">Build selected recipes</button>
107 </div>
108
109 <div class="well well-transparent">
110 <h3>Project release</h3>
111
112 <p class="lead"><span id="project-release-title"></span>
113
114 <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
115
116 <!--i title="" data-original-title="" id="release-change-toggle" class="icon-pencil"></i-->
117 </p>
118
119 <!-- Comment out the ability to change the project release, until we decide what to do with this functionality -->
120
121 <!--form class="form-inline" id="change-release-form" style="display:none;">
122 <select></select>
123 <button class="btn" style="margin-left:5px;" id="change-release-btn">Change</button> <a href="#" id="cancel-release-change" class="btn btn-link">Cancel</a>
124 </form-->
125 </div>
126 </div>
127
128 <div class="col-md-6">
129 <div class="well well-transparent" id="layer-container">
130 <h3>Layers <span class="counter">(<span id="project-layers-count"></span>)</span>
131 <span title="OpenEmbedded organises recipes and machines into thematic groups called <strong>layers</strong>. Click on a layer name to see the recipes and machines it includes." class="glyphicon glyphicon-question-sign get-help"></span>
132 </h3>
133
134 <div class="alert alert-warning" id="no-layers-in-project" style="display:none">
135 <h4>This project has no layers</h4>
136 In order to build this project you need to add some layers first. For that you can:
137 <ul>
138 <li><a href="{% url 'projectlayers' project.id %}">Choose from the layers compatible with this project</a></li>
139 <li><a href="{% url 'importlayer' project.id %}">Import a layer</a></li>
140 <li><a href="http://www.yoctoproject.org/docs/current/dev-manual/dev-manual.html#understanding-and-creating-layers" target="_blank">Read about layers in the documentation</a></li>
141 <li>Or type a layer name below</li>
142 </ul>
143 </div>
144
145 <form class="form-inline">
146 <div class="form-group">
147 <input id="layer-add-input" class="form-control" autocomplete="off" placeholder="Type a layer name" data-minlength="1" data-autocomplete="off" data-provide="typeahead" data-source="" type="text">
148 </div>
149 <button id="add-layer-btn" class="btn btn-default" disabled>Add layer</button>
150 <p class="form-link">
151 <a href="{% url 'projectlayers' project.id %}" id="view-compatible-layers">View compatible layers</a>
152 <span class="text-muted">|</span>
153 <a href="{% url 'importlayer' project.id %}">Import layer</a>
154 </p>
155 </form>
156
157 <ul class="list-unstyled lead" id="layers-in-project-list">
158 </ul>
159 </div>
160
161</div>
162{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html b/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html
new file mode 100644
index 0000000000..622787c4bc
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/project_specific_topbar.html
@@ -0,0 +1,80 @@
1{% load static %}
2<script src="{% static 'js/projecttopbar.js' %}"></script>
3<script>
4 $(document).ready(function () {
5 var ctx = {
6 numProjectLayers : {{project.get_project_layer_versions.count}},
7 machine : "{{project.get_current_machine_name|default_if_none:""}}",
8 }
9
10 try {
11 projectTopBarInit(ctx);
12 } catch (e) {
13 document.write("Sorry, An error has occurred loading this page (pstb):"+e);
14 console.warn(e);
15 }
16 });
17</script>
18
19<div class="col-md-12">
20 <div class="alert alert-success alert-dismissible change-notification" id="project-created-notification" style="display:none">
21 <button type="button" class="close" data-dismiss="alert">&times;</button>
22 <p>Your project <strong>{{project.name}}</strong> has been created. You can now <a class="alert-link" href="{% url 'projectmachines' project.id %}">select your target machine</a> and <a class="alert-link" href="{% url 'projectimagerecipes' project.id %}">choose image recipes</a> to build.</p>
23 </div>
24 <!-- project name -->
25 <div class="page-header">
26 <h1 id="project-name-container">
27 <span class="project-name">{{project.name}}</span>
28 {% if project.is_default %}
29 <span class="glyphicon glyphicon-question-sign get-help" title="This project shows information about the builds you start from the command line while Toaster is running"></span>
30 {% endif %}
31 </h1>
32 <form id="project-name-change-form" class="form-inline" style="display: none;">
33 <div class="form-group">
34 <input class="form-control input-lg" type="text" id="project-name-change-input" autocomplete="off" value="{{project.name}}">
35 </div>
36 <button id="project-name-change-btn" class="btn btn-default btn-lg" type="button">Save</button>
37 <a href="#" id="project-name-change-cancel" class="btn btn-lg btn-link">Cancel</a>
38 </form>
39 </div>
40
41 {% with mrb_type='project' %}
42 {% include "mrb_section.html" %}
43 {% endwith %}
44
45 {% if not project.is_default %}
46 <div id="project-topbar">
47 <ul class="nav nav-tabs">
48 <li id="topbar-configuration-tab">
49 <a href="{% url 'project_specific' project.id %}">
50 Configuration
51 </a>
52 </li>
53 <li>
54 <a href="{% url 'importlayer' project.id %}">
55 Import layer
56 </a>
57 </li>
58 <li>
59 <a href="{% url 'newcustomimage' project.id %}">
60 New custom image
61 </a>
62 </li>
63 <li class="pull-right">
64 <form class="form-inline">
65 <div class="form-group">
66 <span class="glyphicon glyphicon-question-sign get-help" data-placement="left" title="Type the name of one or more recipes you want to build, separated by a space. You can also specify a task by appending a colon and a task name to the recipe name, like so: <code>busybox:clean</code>"></span>
67 <input id="build-input" type="text" class="form-control input-lg" placeholder="Select the default image recipe" autocomplete="off" disabled value="{{project.get_default_image}}">
68 </div>
69 {% if project.get_is_new %}
70 <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Prepare Project</button>
71 {% else %}
72 <button id="cancel-project-button" class="btn info btn-lg" data-project-id="{{project.id}}">Cancel</button>
73 <button id="update-project-button" class="btn btn-primary btn-lg" data-project-id="{{project.id}}">Update</button>
74 {% endif %}
75 </form>
76 </li>
77 </ul>
78 </div>
79 {% endif %}
80</div>
diff --git a/bitbake/lib/toaster/toastergui/templates/projectconf.html b/bitbake/lib/toaster/toastergui/templates/projectconf.html
index 933c588f34..fb20b26f22 100644
--- a/bitbake/lib/toaster/toastergui/templates/projectconf.html
+++ b/bitbake/lib/toaster/toastergui/templates/projectconf.html
@@ -1,4 +1,4 @@
1{% extends "baseprojectpage.html" %} 1{% extends project_specific|yesno:"baseprojectspecificpage.html,baseprojectpage.html" %}
2{% load projecttags %} 2{% load projecttags %}
3{% load humanize %} 3{% load humanize %}
4 4
@@ -438,8 +438,11 @@ function onEditPageUpdate(data) {
438 var_context='m'; 438 var_context='m';
439 } 439 }
440 } 440 }
441 if (configvars_sorted[i][0].startsWith("INTERNAL_")) {
442 var_context='m';
443 }
441 if (var_context == undefined) { 444 if (var_context == undefined) {
442 orightml += '<dt><span id="config_var_entry_'+configvars_sorted[i][2]+'" class="js-config-var-name"></span><span class="glyphicon glyphicon-trash js-icon-trash-config_var" id="config_var_trash_'+configvars_sorted[i][2]+'" x-data="'+configvars_sorted[i][2]+'"></span> </dt>' 445 orightml += '<dt><span id="config_var_entry_'+configvars_sorted[i][2]+'" class="js-config-var-name"></span><span class="glyphicon glyphicon-trash js-icon-trash-config_var" id="config_var_trash_'+configvars_sorted[i][2]+'" x-data="'+configvars_sorted[i][2]+'"></span> </dt>'
443 orightml += '<dd class="variable-list">' 446 orightml += '<dd class="variable-list">'
444 orightml += ' <span class="lead" id="config_var_value_'+configvars_sorted[i][2]+'"></span>' 447 orightml += ' <span class="lead" id="config_var_value_'+configvars_sorted[i][2]+'"></span>'
445 orightml += ' <span class="glyphicon glyphicon-edit js-icon-pencil-config_var" x-data="'+configvars_sorted[i][2]+'"></span>' 448 orightml += ' <span class="glyphicon glyphicon-edit js-icon-pencil-config_var" x-data="'+configvars_sorted[i][2]+'"></span>'
diff --git a/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html b/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html
new file mode 100644
index 0000000000..06c464561e
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/recipe_add_btn.html
@@ -0,0 +1,23 @@
1<a data-recipe-name="{{data.name}}" class="btn btn-default btn-block layer-exists-{{data.layer_version.pk}} set-default-recipe-btn" style="margin-top: 5px;
2 {% if data.layer_version.pk not in extra.current_layers %}
3 display:none;
4 {% endif %}"
5 >
6 Set recipe
7</a>
8<a class="btn btn-default btn-block layerbtn layer-add-{{data.layer_version.pk}}"
9 data-layer='{
10 "id": {{data.layer_version.pk}},
11 "name": "{{data.layer_version.layer.name}}",
12 "layerdetailurl": "{%url "layerdetails" extra.pid data.layer_version.pk%}",
13 "xhrLayerUrl": "{% url "xhr_layer" extra.pid data.layer_version.pk %}"
14 }' data-directive="add"
15 {% if data.layer_version.pk in extra.current_layers %}
16 style="display:none;"
17 {% endif %}
18>
19 <span class="glyphicon glyphicon-plus"></span>
20 Add layer
21 <span class="glyphicon glyphicon-question-sign get-help" title="To set this
22 recipe you must first add the {{data.layer_version.layer.name}} layer to your project"></i>
23</a>
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index e07b0efc1f..dc03e30356 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -116,6 +116,11 @@ urlpatterns = [
116 tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"), 116 tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"),
117 name='projectbuilds'), 117 name='projectbuilds'),
118 118
119 url(r'^newproject_specific/(?P<pid>\d+)/$', views.newproject_specific, name='newproject_specific'),
120 url(r'^project_specific/(?P<pid>\d+)/$', views.project_specific, name='project_specific'),
121 url(r'^landing_specific/(?P<pid>\d+)/$', views.landing_specific, name='landing_specific'),
122 url(r'^landing_specific_cancel/(?P<pid>\d+)/$', views.landing_specific_cancel, name='landing_specific_cancel'),
123
119 # the import layer is a project-specific functionality; 124 # the import layer is a project-specific functionality;
120 url(r'^project/(?P<pid>\d+)/importlayer$', views.importlayer, name='importlayer'), 125 url(r'^project/(?P<pid>\d+)/importlayer$', views.importlayer, name='importlayer'),
121 126
@@ -233,6 +238,14 @@ urlpatterns = [
233 api.XhrBuildRequest.as_view(), 238 api.XhrBuildRequest.as_view(),
234 name='xhr_buildrequest'), 239 name='xhr_buildrequest'),
235 240
241 url(r'^xhr_projectupdate/project/(?P<pid>\d+)$',
242 api.XhrProjectUpdate.as_view(),
243 name='xhr_projectupdate'),
244
245 url(r'^xhr_setdefaultimage/project/(?P<pid>\d+)$',
246 api.XhrSetDefaultImageUrl.as_view(),
247 name='xhr_setdefaultimage'),
248
236 url(r'xhr_project/(?P<project_id>\d+)$', 249 url(r'xhr_project/(?P<project_id>\d+)$',
237 api.XhrProject.as_view(), 250 api.XhrProject.as_view(),
238 name='xhr_project'), 251 name='xhr_project'),
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 34ed2b2e3c..4939b6b1f4 100755..100644
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -25,6 +25,7 @@ import re
25from django.db.models import F, Q, Sum 25from django.db.models import F, Q, Sum
26from django.db import IntegrityError 26from django.db import IntegrityError
27from django.shortcuts import render, redirect, get_object_or_404 27from django.shortcuts import render, redirect, get_object_or_404
28from django.utils.http import urlencode
28from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe 29from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe
29from orm.models import LogMessage, Variable, Package_Dependency, Package 30from orm.models import LogMessage, Variable, Package_Dependency, Package
30from orm.models import Task_Dependency, Package_File 31from orm.models import Task_Dependency, Package_File
@@ -51,6 +52,7 @@ logger = logging.getLogger("toaster")
51 52
52# Project creation and managed build enable 53# Project creation and managed build enable
53project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) 54project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
55is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
54 56
55class MimeTypeFinder(object): 57class MimeTypeFinder(object):
56 # setting this to False enables additional non-standard mimetypes 58 # setting this to False enables additional non-standard mimetypes
@@ -70,6 +72,7 @@ class MimeTypeFinder(object):
70# single point to add global values into the context before rendering 72# single point to add global values into the context before rendering
71def toaster_render(request, page, context): 73def toaster_render(request, page, context):
72 context['project_enable'] = project_enable 74 context['project_enable'] = project_enable
75 context['project_specific'] = is_project_specific
73 return render(request, page, context) 76 return render(request, page, context)
74 77
75 78
@@ -1434,12 +1437,160 @@ if True:
1434 1437
1435 raise Exception("Invalid HTTP method for this page") 1438 raise Exception("Invalid HTTP method for this page")
1436 1439
1440 # new project
1441 def newproject_specific(request, pid):
1442 if not project_enable:
1443 return redirect( landing )
1444
1445 project = Project.objects.get(pk=pid)
1446 template = "newproject_specific.html"
1447 context = {
1448 'email': request.user.email if request.user.is_authenticated() else '',
1449 'username': request.user.username if request.user.is_authenticated() else '',
1450 'releases': Release.objects.order_by("description"),
1451 'projectname': project.name,
1452 'project_pk': project.pk,
1453 }
1454
1455 # WORKAROUND: if we already know release, redirect 'newproject_specific' to 'project_specific'
1456 if '1' == project.get_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE'):
1457 return redirect(reverse(project_specific, args=(project.pk,)))
1458
1459 try:
1460 context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value
1461 except ToasterSetting.DoesNotExist:
1462 pass
1463
1464 if request.method == "GET":
1465 # render new project page
1466 return toaster_render(request, template, context)
1467 elif request.method == "POST":
1468 mandatory_fields = ['projectname', 'ptype']
1469 try:
1470 ptype = request.POST.get('ptype')
1471 if ptype == "build":
1472 mandatory_fields.append('projectversion')
1473 # make sure we have values for all mandatory_fields
1474 missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0]
1475 if missing:
1476 # set alert for missing fields
1477 raise BadParameterException("Fields missing: %s" % ", ".join(missing))
1478
1479 if not request.user.is_authenticated():
1480 user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass')
1481 if user is None:
1482 user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass")
1483
1484 user = authenticate(username = user.username, password = 'nopass')
1485 login(request, user)
1486
1487 # save the project
1488 if ptype == "analysis":
1489 release = None
1490 else:
1491 release = Release.objects.get(pk = request.POST.get('projectversion', None ))
1492
1493 prj = Project.objects.create_project(name = request.POST['projectname'], release = release, existing_project = project)
1494 prj.user_id = request.user.pk
1495 prj.save()
1496 return redirect(reverse(project_specific, args=(prj.pk,)) + "?notify=new-project")
1497
1498 except (IntegrityError, BadParameterException) as e:
1499 # fill in page with previously submitted values
1500 for field in mandatory_fields:
1501 context.__setitem__(field, request.POST.get(field, "-- missing"))
1502 if isinstance(e, IntegrityError) and "username" in str(e):
1503 context['alert'] = "Your chosen username is already used"
1504 else:
1505 context['alert'] = str(e)
1506 return toaster_render(request, template, context)
1507
1508 raise Exception("Invalid HTTP method for this page")
1509
1437 # Shows the edit project page 1510 # Shows the edit project page
1438 def project(request, pid): 1511 def project(request, pid):
1439 project = Project.objects.get(pk=pid) 1512 project = Project.objects.get(pk=pid)
1513
1514 if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
1515 if request.GET:
1516 #Example:request.GET=<QueryDict: {'setMachine': ['qemuarm']}>
1517 params = urlencode(request.GET).replace('%5B%27','').replace('%27%5D','')
1518 return redirect("%s?%s" % (reverse(project_specific, args=(project.pk,)),params))
1519 else:
1520 return redirect(reverse(project_specific, args=(project.pk,)))
1440 context = {"project": project} 1521 context = {"project": project}
1441 return toaster_render(request, "project.html", context) 1522 return toaster_render(request, "project.html", context)
1442 1523
1524 # Shows the edit project-specific page
1525 def project_specific(request, pid):
1526 project = Project.objects.get(pk=pid)
1527
1528 # Are we refreshing from a successful project specific update clone?
1529 if Project.PROJECT_SPECIFIC_CLONING_SUCCESS == project.get_variable(Project.PROJECT_SPECIFIC_STATUS):
1530 return redirect(reverse(landing_specific,args=(project.pk,)))
1531
1532 context = {
1533 "project": project,
1534 "is_new" : project.get_variable(Project.PROJECT_SPECIFIC_ISNEW),
1535 "default_image_recipe" : project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE),
1536 "mru" : Build.objects.all().filter(project=project,outcome=Build.IN_PROGRESS),
1537 }
1538 if project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1539 context['build_in_progress_none_completed'] = True
1540 else:
1541 context['build_in_progress_none_completed'] = False
1542 return toaster_render(request, "project.html", context)
1543
1544 # perform the final actions for the project specific page
1545 def project_specific_finalize(cmnd, pid):
1546 project = Project.objects.get(pk=pid)
1547 callback = project.get_variable(Project.PROJECT_SPECIFIC_CALLBACK)
1548 if "update" == cmnd:
1549 # Delete all '_PROJECT_PREPARE_' builds
1550 for b in Build.objects.all().filter(project=project):
1551 delete_build = False
1552 for t in b.target_set.all():
1553 if '_PROJECT_PREPARE_' == t.target:
1554 delete_build = True
1555 if delete_build:
1556 from django.core import management
1557 management.call_command('builddelete', str(b.id), interactive=False)
1558 # perform callback at this last moment if defined, in case Toaster gets shutdown next
1559 default_target = project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
1560 if callback:
1561 callback = callback.replace("<IMAGE>",default_target)
1562 if "cancel" == cmnd:
1563 if callback:
1564 callback = callback.replace("<IMAGE>","none")
1565 callback = callback.replace("--update","--cancel")
1566 # perform callback at this last moment if defined, in case this Toaster gets shutdown next
1567 ret = ''
1568 if callback:
1569 ret = os.system('bash -c "%s"' % callback)
1570 project.set_variable(Project.PROJECT_SPECIFIC_CALLBACK,'')
1571 # Delete the temp project specific variables
1572 project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,'')
1573 project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_NONE)
1574 # WORKAROUND: Release this workaround flag
1575 project.set_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE','')
1576
1577 # Shows the final landing page for project specific update
1578 def landing_specific(request, pid):
1579 project_specific_finalize("update", pid)
1580 context = {
1581 "install_dir": os.environ['TOASTER_DIR'],
1582 }
1583 return toaster_render(request, "landing_specific.html", context)
1584
1585 # Shows the related landing-specific page
1586 def landing_specific_cancel(request, pid):
1587 project_specific_finalize("cancel", pid)
1588 context = {
1589 "install_dir": os.environ['TOASTER_DIR'],
1590 "status": "cancel",
1591 }
1592 return toaster_render(request, "landing_specific.html", context)
1593
1443 def jsunittests(request): 1594 def jsunittests(request):
1444 """ Provides a page for the js unit tests """ 1595 """ Provides a page for the js unit tests """
1445 bbv = BitbakeVersion.objects.filter(branch="master").first() 1596 bbv = BitbakeVersion.objects.filter(branch="master").first()
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index a1792d997f..88dff8a857 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -89,6 +89,10 @@ class ToasterTable(TemplateView):
89 89
90 # global variables 90 # global variables
91 context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER')) 91 context['project_enable'] = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
92 try:
93 context['project_specific'] = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
94 except:
95 context['project_specific'] = ''
92 96
93 return context 97 return context
94 98
@@ -519,6 +523,8 @@ class MostRecentBuildsView(View):
519 int((build_obj.repos_cloned / 523 int((build_obj.repos_cloned /
520 build_obj.repos_to_clone) * 100) 524 build_obj.repos_to_clone) * 100)
521 525
526 build['progress_item'] = build_obj.progress_item
527
522 tasks_complete_percentage = 0 528 tasks_complete_percentage = 0
523 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED): 529 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
524 tasks_complete_percentage = 100 530 tasks_complete_percentage = 100
diff --git a/bitbake/lib/toaster/toastermain/management/commands/builddelete.py b/bitbake/lib/toaster/toastermain/management/commands/builddelete.py
index 0bef8d4103..bf69a8fb80 100644
--- a/bitbake/lib/toaster/toastermain/management/commands/builddelete.py
+++ b/bitbake/lib/toaster/toastermain/management/commands/builddelete.py
@@ -10,8 +10,12 @@ class Command(BaseCommand):
10 args = '<buildID1 buildID2 .....>' 10 args = '<buildID1 buildID2 .....>'
11 help = "Deletes selected build(s)" 11 help = "Deletes selected build(s)"
12 12
13 def add_arguments(self, parser):
14 parser.add_argument('buildids', metavar='N', type=int, nargs='+',
15 help="Build ID's to delete")
16
13 def handle(self, *args, **options): 17 def handle(self, *args, **options):
14 for bid in args: 18 for bid in options['buildids']:
15 try: 19 try:
16 b = Build.objects.get(pk = bid) 20 b = Build.objects.get(pk = bid)
17 except ObjectDoesNotExist: 21 except ObjectDoesNotExist:
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