def run(self, env): try: self.task.started(TYPE) for extension in self.task.extensions: extension.started(TYPE) with hooks.task_run([self.task] + self.task.extensions): self._run(env) for extension in self.task.extensions: extension.finished(TYPE) self.task.finished(TYPE) except (ConnectionError, AMQPConnectionError): log.exception() for extension in self.task.extensions: extension.failed(TYPE) self.task.failed(TYPE) raise_error("Lost connection to AMQP server") except Exception as e: log.exception() for extension in self.task.extensions: extension.failed(TYPE) self.task.failed(TYPE) raise e finally: if self.connection is not None: utils.call_and_catch(self.connection.close) return self.task
def _find_dotgit(self, path): ppath = None while path != ppath: if fs.path.isdir(fs.path.join(path, ".git")): return path ppath = path path = fs.path.dirname(path) raise_error("no git repository found at '{}'", self.path)
def _list(ctx, task=None, all=False, reverse=None): """ List all tasks, or dependencies of a task. By default, when no TASK is specified, all known task names are listed in alphabetical order. When a TASK is specified, only direct dependencies of that task are listed. Use -a to also list its indirect dependencies. Multiple TASK names are allowed. """ raise_error_if(not task and reverse, "TASK required with --reverse") registry = TaskRegistry.get() if not task: classes = registry.get_task_classes() for task in sorted(classes, key=lambda x: x.name): if task.name: print(task.name) return task = [utils.stable_task_name(t) for t in task] reverse = [utils.stable_task_name(t) for t in utils.as_list(reverse or [])] try: dag = graph.GraphBuilder(registry, ctx.obj["manifest"]).build(task, influence=False) except JoltError as e: raise e except Exception: raise_error( "an exception occurred during task dependency evaluation, see log for details" ) task = reverse or task nodes = dag.select(lambda graph, node: node.short_qualified_name in task or node.qualified_name in task) nodes = list(nodes) iterator = dag.predecessors if reverse else dag.successors tasklist = set() while nodes: node = nodes.pop() for task in iterator(node): if all and task.short_qualified_name not in tasklist: new_node = dag.get_task(task.qualified_name) nodes.append(new_node) tasklist.add(task.short_qualified_name) for task in sorted(list(tasklist)): print(task)
def include(joltfile): """ Include another Python file """ try: from os import path from sys import _getframe from jolt.loader import JoltLoader filepath = _getframe().f_back.f_code.co_filename filepath = path.dirname(filepath) filepath = path.join(filepath, joltfile) JoltLoader.get()._load_file(filepath) except Exception as e: from jolt.error import raise_error raise_error("failed to load '{0}': {1}", joltfile, str(e))
def _run(self): job = self._queue.get(False) self._queue.task_done() try: if not self.is_aborted(): job.executor.run(job.env) except KeyboardInterrupt as e: raise_error("Interrupted by user") self._aborted = True job.future.set_exception(e) except Exception as e: if not self.is_keep_going(): self._aborted = True job.future.set_exception(e) else: job.future.set_result(job.executor)
def rev_parse(self, rev): if self.is_valid_sha(rev): return rev with self.tools.cwd(self.path): try: commit = self.repository.revparse_single(rev) except KeyError: self.fetch() try: commit = self.repository.revparse_single(rev) except Exception: raise_error("invalid git reference: {}", rev) try: return str(commit.id) except Exception: return str(commit)
def _list(ctx, task=None, reverse=None): """ List all tasks, or dependencies of a task. """ raise_error_if(not task and reverse, "TASK required with --reverse") registry = TaskRegistry.get() if not task: classes = registry.get_task_classes() classes += registry.get_test_classes() for task in sorted(classes, key=lambda x: x.name): if task.name: print(task.name) return task = [utils.stable_task_name(t) for t in task] reverse = [utils.stable_task_name(t) for t in utils.as_list(reverse or [])] try: dag = graph.GraphBuilder(registry, ctx.obj["manifest"]).build(task, influence=False) except JoltError as e: raise e except Exception: raise_error( "an exception occurred during task dependency evaluation, see log for details" ) task = reverse or task nodes = dag.select(lambda graph, node: node.short_qualified_name in task or node.qualified_name in task) tasklist = set() iterator = dag.predecessors if reverse else dag.successors for node in nodes: for task in iterator(node): tasklist.add(task.short_qualified_name) for task in sorted(list(tasklist)): print(task)
def parse_aliased_task_name(name): match = re.match( r"^((?P<alias>[^=:]+)=)?(?P<task>[^:]+)(:(?P<params>.*))?$", name) if not match: from jolt.error import raise_error raise_error("Illegal task name: {}", name) match = match.groupdict() alias = match["alias"] task = match["task"] params = match["params"] or {} if params: params = params.split(",") def _param(param): if "=" in param: key, value = param.split("=", 1) else: key, value = param, None return key, value params = {key: value for key, value in map(_param, params) if key} return alias, task, params
def _config(ctx, list, delete, global_, user, key, value): """ Configure Jolt. You can query/set/replace/unset configuration keys with this command. Key strings are constructed from the configuration section and the option separated by a dot. There are tree different configuration sources: - A global configuration file - A user configuration file - Temporary configuration passed on the command line. When reading, the values are read from all configuration sources. The options --global and --user can be used to tell the command to read from only one of the sources. If a configuration key is available from multiple sources, temporary CLI configuration has priority followed by the user configuration file and lastly the global configuration file. When writing, the new values are written to the user configuration by default. The options --global and --user can be used to tell the command to write to only one of the sources. When removing keys, the values are removed from all sources. The options --global and --user can be used to restrict removal to one of the sources. To assign a value to a key: $ jolt config jolt.default all # Change name of the default task To list existing keys: $ jolt config -l # List all existing keys $ jolt config -l -g # List keys in the global config file $ jolt config jolt.colors # Display the value of a key. To delete an existing key: $ jolt config -d jolt.colors To pass temporary configuration: $ jolt -c jolt.colors=true config -l """ if delete and not key: raise click.UsageError("--delete requires KEY") if not key and not list and not key: print(ctx.get_help()) sys.exit(1) if global_ and user: raise click.UsageError("--global and --user are mutually exclusive") alias = None if global_: alias = "global" if user: alias = "user" def _print_key(section, opt): value = config.get(section, opt, alias=alias) raise_error_if(value is None, "no such key: {}".format(key)) print("{} = {}".format(key, value)) def _print_section(section): print("{}".format(section)) if list: for section, option, value in config.items(alias): if option: print("{}.{} = {}".format(section, option, value)) else: print(section) elif delete: raise_error_if(config.delete(key, alias) <= 0, "no such key: {}", key) config.save() elif key: section, opt = config.split(key) if value: raise_error_if(opt is None, "invalid configuration key: {}".format(key)) config.set(section, opt, value, alias) try: config.save() except Exception as e: log.exception() raise_error("failed to write configuration file: {}".format(e)) else: if opt: _print_key(section, opt) else: _print_section(section)
def build(ctx, task, network, keep_going, default, local, no_download, no_upload, download, upload, worker, force, salt, copy, debug, result, jobs): """ Build task artifact. TASK is the name of the task to execute. It is optionally followed by a colon and parameter value assignments. Assignments are separated by commas. Example: taskname:param1=value1,param2=value2 Default parameter values can be overridden for any task in the dependency tree with --default. DEFAULT is a qualified task name, just like TASK, but parameter assignments change default values. By default, a task is executed locally and the resulting artifact is stored in the local artifact cache. If an artifact is already available in the cache, no execution takes place. Artifacts are identified with a hash digest, constructed from hashing task attributes. When remote cache providers are configured, artifacts may be downloaded from and/or uploaded to the remote cache as execution progresses. Several options exist to control the behavior, such as --local which disables all remote caches. Distributed task execution is enabled by passing the --network option. Tasks are then distributed to and executed by a pool of workers, if one has been configured. Rebuilds can be forced with either --force or --salt. --force rebuilds the requested task, but not its dependencies. --salt affects the entire dependency tree. Both add an extra attribute to the task hash calculation in order to taint the identity and induce a cache miss. In both cases, existing intermediate files in build directories are removed before execution starts. """ raise_error_if(network and local, "The -n and -l flags are mutually exclusive") raise_error_if(network and debug, "The -g and -n flags are mutually exclusive") raise_error_if( no_download and download, "The --download and --no-download flags are mutually exclusive") raise_error_if( no_upload and upload, "The --upload and --no-upload flags are mutually exclusive") duration = utils.duration() task = list(task) task = [utils.stable_task_name(t) for t in task] if network: _download = config.getboolean("network", "download", True) _upload = config.getboolean("network", "upload", True) else: _download = config.getboolean("jolt", "download", True) _upload = config.getboolean("jolt", "upload", True) if local: _download = False _upload = False else: if no_download: _download = False if no_upload: _upload = False if download: _download = True if upload: _upload = True options = JoltOptions(network=network, local=local, download=_download, upload=_upload, keep_going=keep_going, default=default, worker=worker, debug=debug, salt=salt, jobs=jobs) acache = cache.ArtifactCache.get(options) executors = scheduler.ExecutorRegistry.get(options) if worker: log.set_worker() log.verbose("Local build as a worker") strategy = scheduler.WorkerStrategy(executors, acache) elif network: log.verbose("Distributed build as a user") strategy = scheduler.DistributedStrategy(executors, acache) else: log.verbose("Local build as a user") strategy = scheduler.LocalStrategy(executors, acache) hooks.TaskHookRegistry.get(options) registry = TaskRegistry.get(options) for params in default: registry.set_default_parameters(params) manifest = ctx.obj["manifest"] for mb in manifest.builds: for mt in mb.tasks: task.append(mt.name) for mt in mb.defaults: registry.set_default_parameters(mt.name) if force: for goal in task: registry.get_task(goal, manifest=manifest).taint = uuid.uuid4() gb = graph.GraphBuilder(registry, manifest, options, progress=True) dag = gb.build(task) gp = graph.GraphPruner(strategy) dag = gp.prune(dag) goal_tasks = dag.goals goal_task_duration = 0 queue = scheduler.TaskQueue(strategy) try: if not dag.has_tasks(): return progress = log.progress( "Progress", dag.number_of_tasks(filterfn=lambda t: not t.is_resource()), " tasks", estimates=False, debug=debug) with progress: while dag.has_tasks(): # Find all tasks ready to be executed leafs = dag.select(lambda graph, task: task.is_ready()) # Order the tasks by their weights to improve build times leafs.sort(key=lambda x: x.weight) while leafs: task = leafs.pop() queue.submit(acache, task) task, error = queue.wait() if not task: dag.debug() break elif task.is_goal() and task.duration_running: goal_task_duration += task.duration_running.seconds if not task.is_resource(): progress.update(1) if not keep_going and error is not None: queue.abort() raise error if dag.failed: log.error("List of failed tasks") for failed in dag.failed: log.error("- {}", failed.log_name.strip("()")) raise_error("no more tasks could be executed") for goal in goal_tasks: if acache.is_available_locally(goal): with acache.get_artifact(goal) as artifact: log.info("Location: {0}", artifact.path) if copy: artifact.copy("*", utils.as_dirpath( fs.path.join( workdir, click.format_filename(copy))), symlinks=True) except KeyboardInterrupt: print() log.warning("Interrupted by user") try: queue.abort() sys.exit(1) except KeyboardInterrupt: print() log.warning("Interrupted again, exiting") _exit(1) finally: log.info("Total execution time: {0} {1}", str(duration), str(queue.duration_acc) if network else '') if result: with report.update() as manifest: manifest.duration = str(goal_task_duration) manifest.write(result)
def _run(self, env): timeout = int(config.getint("amqp", "timeout", 300)) manifest, routing_key = self._create_manifest() self.connect() self.publish_request(manifest, routing_key) log.debug("[AMQP] Queued {0}", self.task.short_qualified_name) self.task.running() for extension in self.task.extensions: extension.running() while self.response is None: try: self.connection.process_data_events(time_limit=timeout) if self.response is None: self.task.info( "Remote execution still in progress after {}", self.task.duration_queued) except (ConnectionError, AMQPConnectionError): log.warning("[AMQP] Lost server connection") self.connect() log.debug("[AMQP] Finished {0}", self.task.short_qualified_name) manifest = JoltManifest() with raise_task_error_on_exception( self.task, "failed to parse build result manifest"): manifest.parsestring(self.response) self.task.running(utils.duration() - float(manifest.duration)) if manifest.result != "SUCCESS": output = [] if manifest.stdout: output.extend(manifest.stdout.split("\n")) if manifest.stderr: output.extend(manifest.stderr.split("\n")) for line in output: log.transfer(line, self.task.identity[:8]) for task in [self.task] + self.task.extensions: with task.task.report() as report: remote_report = manifest.find_task(task.qualified_name) if remote_report: for error in remote_report.errors: report.manifest.append(error) raise_error("[AMQP] remote build failed with status: {0}".format( manifest.result)) raise_task_error_if( not env.cache.is_available_remotely(self.task), self.task, "no task artifact available in any cache, check configuration") raise_task_error_if( not env.cache.download(self.task) and env.cache.download_enabled(), self.task, "failed to download task artifact") for extension in self.task.extensions: raise_task_error_if( not env.cache.download(extension) and env.cache.download_enabled(), self.task, "failed to download task artifact") return self.task