diff options
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 | |||
