diff options
Diffstat (limited to 'bitbake/lib')
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 | |||
27 | import time | 27 | import time |
28 | from django.db import transaction | 28 | from django.db import transaction |
29 | from django.db.models import Q | 29 | from django.db.models import Q |
30 | from bldcontrol.models import BuildEnvironment, BRLayer, BRVariable, BRTarget, BRBitbake | 30 | from bldcontrol.models import BuildEnvironment, BuildRequest, BRLayer, BRVariable, BRTarget, BRBitbake, Build |
31 | from orm.models import CustomImageRecipe, Layer, Layer_Version, ProjectLayer, ToasterSetting | 31 | from orm.models import CustomImageRecipe, Layer, Layer_Version, Project, ProjectLayer, ToasterSetting |
32 | from orm.models import signal_runbuilds | ||
32 | import subprocess | 33 | import subprocess |
33 | 34 | ||
34 | from toastermain import settings | 35 | from toastermain import settings |
@@ -38,6 +39,8 @@ from bldcontrol.bbcontroller import BuildEnvironmentController, ShellCmdExceptio | |||
38 | import logging | 39 | import logging |
39 | logger = logging.getLogger("toaster") | 40 | logger = logging.getLogger("toaster") |
40 | 41 | ||
42 | install_dir = os.environ.get('TOASTER_DIR') | ||
43 | |||
41 | from pprint import pprint, pformat | 44 | from pprint import pprint, pformat |
42 | 45 | ||
43 | class LocalhostBEController(BuildEnvironmentController): | 46 | class 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 -*- | ||
2 | from __future__ import unicode_literals | ||
3 | |||
4 | from django.db import migrations, models | ||
5 | |||
6 | class 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 | ||
123 | class ProjectManager(models.Manager): | 123 | class 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 | |||
22 | import re | 22 | import re |
23 | import logging | 23 | import logging |
24 | import json | 24 | import json |
25 | import subprocess | ||
25 | from collections import Counter | 26 | from collections import Counter |
27 | from shutil import copyfile | ||
26 | 28 | ||
27 | from orm.models import Project, ProjectTarget, Build, Layer_Version | 29 | from orm.models import Project, ProjectTarget, Build, Layer_Version |
28 | from orm.models import LayerVersionDependency, LayerSource, ProjectLayer | 30 | from orm.models import LayerVersionDependency, LayerSource, ProjectLayer |
@@ -38,6 +40,18 @@ from django.core.urlresolvers import reverse | |||
38 | from django.db.models import Q, F | 40 | from django.db.models import Q, F |
39 | from django.db import Error | 41 | from django.db import Error |
40 | from toastergui.templatetags.projecttags import filtered_filesizeformat | 42 | from toastergui.templatetags.projecttags import filtered_filesizeformat |
43 | from django.utils import timezone | ||
44 | import pytz | ||
45 | |||
46 | # development/debugging support | ||
47 | verbose = 2 | ||
48 | def _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 | ||
42 | logger = logging.getLogger("toaster") | 56 | logger = logging.getLogger("toaster") |
43 | 57 | ||
@@ -137,6 +151,130 @@ class XhrBuildRequest(View): | |||
137 | return response | 151 | return response |
138 | 152 | ||
139 | 153 | ||
154 | class 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 | |||
197 | class 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 | |||
249 | def 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 | |||
140 | class XhrLayer(View): | 278 | class 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 | |||
35 | from toastergui.tablefilter import TableFilterActionDateRange | 35 | from toastergui.tablefilter import TableFilterActionDateRange |
36 | from toastergui.tablefilter import TableFilterActionDay | 36 | from toastergui.tablefilter import TableFilterActionDay |
37 | 37 | ||
38 | import os | ||
39 | |||
38 | class ProjectFilters(object): | 40 | class 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">×</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 | 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">×</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 | |||
25 | from django.db.models import F, Q, Sum | 25 | from django.db.models import F, Q, Sum |
26 | from django.db import IntegrityError | 26 | from django.db import IntegrityError |
27 | from django.shortcuts import render, redirect, get_object_or_404 | 27 | from django.shortcuts import render, redirect, get_object_or_404 |
28 | from django.utils.http import urlencode | ||
28 | from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe | 29 | from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe |
29 | from orm.models import LogMessage, Variable, Package_Dependency, Package | 30 | from orm.models import LogMessage, Variable, Package_Dependency, Package |
30 | from orm.models import Task_Dependency, Package_File | 31 | from 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 |
53 | project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) | 54 | project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER')) |
55 | is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC')) | ||
54 | 56 | ||
55 | class MimeTypeFinder(object): | 57 | class 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 |
71 | def toaster_render(request, page, context): | 73 | def 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 | |||
47 | from django.core.management.base import BaseCommand, CommandError | ||
48 | from django.core.exceptions import ObjectDoesNotExist | ||
49 | from orm.models import ProjectManager, Project, Release, ProjectVariable | ||
50 | from orm.models import Layer, Layer_Version, LayerSource, ProjectLayer | ||
51 | from toastergui.api import scan_layer_content | ||
52 | from django.db import OperationalError | ||
53 | |||
54 | import os | ||
55 | import re | ||
56 | import os.path | ||
57 | import subprocess | ||
58 | |||
59 | # Toaster variable section delimiters | ||
60 | TOASTER_PROLOG = '#=== TOASTER_CONFIG_PROLOG ===' | ||
61 | TOASTER_EPILOG = '#=== TOASTER_CONFIG_EPILOG ===' | ||
62 | |||
63 | # quick development/debugging support | ||
64 | verbose = 2 | ||
65 | def _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 | |||
99 | class 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 | |||