Skip to content

Commit ab41f4a

Browse files
committed
tasks: add PREREQ_FOR
In some cases, it is useful to specify a task that would run before another one, if the other task is also running. For instance, this could ensure that a certain item is configured prior to being used, but only for certain environments. Signed-off-by: Stephen Brennan <[email protected]>
1 parent 5fc2151 commit ab41f4a

File tree

5 files changed

+114
-35
lines changed

5 files changed

+114
-35
lines changed

doc/guide/tasks.rst

+11
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ functions which are available to you:
9393
script should not run again. It will detect a previous success, and exit with
9494
code 0. Note that this will still allow you to re-run a failed task.
9595

96+
- ``PREREQ_FOR <other task>`` - this declares that your task is a
97+
prerequisite of another. It can be thought of as the inverse of
98+
``DEPENDS_ON``, but with one important caveat. This relationship only applies
99+
if the other task is actually loaded and run by Yo at the same time as this
100+
one. For example, suppose task ``A`` contains ``PREREQ_FOR B``. Then:
101+
102+
- Specifying task ``A`` will not automatically run task ``B``
103+
- Similarly, specifying task ``B`` will not automatically run ``A``
104+
- However, if task ``A`` and ``B`` are both specified to run, then Yo will
105+
ensure that A runs to completion before task ``B`` begins.
106+
96107
These functions can be used anywhere in your script, however bash syntax such as
97108
quoting or variable expansion is not respected when Yo locates them. So, while
98109
the following is valid bash, it won't work with Yo:

yo/data/yo-tasks/test-deps

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3434
# SOFTWARE.
3535

36-
DEPENDS_ON test-task
3736
MAYBE_DEPENDS_ON test-non-existing-task
3837
MAYBE_DEPENDS_ON test-existing-task
38+
DEPENDS_ON test-task
3939
sleep 10
4040
echo completed

yo/data/yo-tasks/test-prereq

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (c) 2025, Oracle and/or its affiliates.
2+
#
3+
# The Universal Permissive License (UPL), Version 1.0
4+
#
5+
# Subject to the condition set forth below, permission is hereby granted to any
6+
# person obtaining a copy of this software, associated documentation and/or data
7+
# (collectively the "Software"), free of charge and under any and all copyright
8+
# rights in the Software, and any and all patent rights owned or freely
9+
# licensable by each licensor hereunder covering either (i) the unmodified
10+
# Software as contributed to or provided by such licensor, or (ii) the Larger
11+
# Works (as defined below), to deal in both
12+
#
13+
# (a) the Software, and
14+
# (b) any piece of software and/or hardware listed in the
15+
# lrgrwrks.txt file if one is included with the Software (each a "Larger
16+
# Work" to which the Software is contributed by such licensors),
17+
#
18+
# without restriction, including without limitation the rights to copy, create
19+
# derivative works of, display, perform, and distribute the Software and make,
20+
# use, sell, offer for sale, import, export, have made, and have sold the
21+
# Software and the Larger Work(s), and to sublicense the foregoing rights on
22+
# either these or other terms.
23+
#
24+
# This license is subject to the following condition: The above copyright notice
25+
# and either this complete permission notice or at a minimum a reference to the
26+
# UPL must be included in all copies or substantial portions of the Software.
27+
#
28+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34+
# SOFTWARE.
35+
36+
PREREQ_FOR test-existing-task
37+
sleep 10
38+
echo completed

yo/data/yo_tasklib.sh

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ RUN_ONCE() {
3535
fi
3636
}
3737

38+
PREREQ_FOR() {
39+
true
40+
}
41+
3842
# Setup the "$ORAVER" variable for Oracle Linux
3943
if [ -f /etc/oracle-release ] && grep '6\.' /etc/oracle-release; then
4044
ORAVER=6

yo/main.py

+60-34
Original file line numberDiff line numberDiff line change
@@ -484,13 +484,15 @@ class YoTask:
484484
script: str
485485
dependencies: t.List[str]
486486
conflicts: t.List[str]
487+
prereq_for: t.List[str]
487488

488489
@classmethod
489490
def create_from_string(
490491
cls, name: str, script: str, path: str = "(memory)"
491492
) -> "YoTask":
492493
dependencies = []
493494
conflicts = []
495+
prereq_for = []
494496
all_tasks = list_tasks()
495497
lines = script.split("\n")
496498
for i in range(len(lines)):
@@ -509,7 +511,11 @@ def create_from_string(
509511
"MAYBE_DEPENDS_ON", "# MAYBE_DEPENDS_ON"
510512
)
511513
lines[i] = line
512-
return YoTask(name, path, "\n".join(lines), dependencies, conflicts)
514+
elif line.startswith("PREREQ_FOR"):
515+
prereq_for.append(line.split(None, maxsplit=1)[1])
516+
return YoTask(
517+
name, path, "\n".join(lines), dependencies, conflicts, prereq_for
518+
)
513519

514520
@classmethod
515521
@lru_cache(maxsize=None)
@@ -532,6 +538,10 @@ def load(cls, name: str) -> "YoTask":
532538
script = f.read()
533539
return cls.create_from_string(name, script, path=path)
534540

541+
def insert_prereq(self, other: str) -> None:
542+
self.dependencies.append(other)
543+
self.script = f"DEPENDS_ON {other}\n{self.script}"
544+
535545

536546
@lru_cache(maxsize=1)
537547
def list_tasks() -> t.List[str]:
@@ -607,51 +617,67 @@ def _task_run(ctx: YoCtx, inst: YoInstance, task: YoTask) -> None:
607617
def run_all_tasks(
608618
ctx: YoCtx, inst: YoInstance, tasks: t.Iterable[t.Union[YoTask, str]]
609619
) -> None:
610-
# Tasks can have dependencies. We need to be sure to load the full set of
611-
# dependencies in the list of tasks we're given. We also need to verify
612-
# there are no circular dependencies. And it's nice to launch them in order
613-
# that will satisfy their dependencies, but the "DEPENDS_ON" function should
614-
# successfully handle waiting until all depnedencies are completed anyway.
615-
616-
# 0: not visited, 1: visiting, 2: finished visit
620+
# The caller may specify tasks as either strings or YoTask instances, for
621+
# convenience. Let's get everything into a "name_to_task" dict.
622+
name_to_task: t.Dict[str, YoTask] = {}
623+
for task in tasks:
624+
if isinstance(task, str):
625+
name_to_task[task] = YoTask.load(task)
626+
else:
627+
name_to_task[task.name] = task
628+
629+
# Tasks may have dependencies. Let's go through every task, and their
630+
# dependencies, and load them all. At this point, we're not yet checking
631+
# whether there are any circular dependencies: just loading them.
632+
tasks_to_load = list(name_to_task.values())
633+
for task in tasks_to_load:
634+
for name in task.dependencies:
635+
if name not in name_to_task:
636+
name_to_task[name] = YoTask.load(name)
637+
tasks_to_load.append(name_to_task[name])
638+
639+
# Now we have loaded the complete set of tasks that should run. Some tasks
640+
# may appoint themselves as "prerequisites" for another. We need to insert
641+
# this dependency relationship so that the script is updated, and so that
642+
# the circular dependency detection knows about it. We can also use this
643+
# opportunity to detect conflicts.
644+
for task in name_to_task.values():
645+
for name in task.prereq_for:
646+
if name in name_to_task:
647+
name_to_task[name].insert_prereq(task.name)
648+
for name in task.conflicts:
649+
if name in name_to_task:
650+
raise YoExc(f"Task {task.name} conflicts with {name}")
651+
652+
# Now all tasks are loaded, and prerequisites have been marked. Use a
653+
# topological sort to verify that no circular dependencies are present. Here
654+
# we use a recursive traversal because honestly, if you specify enough tasks
655+
# to trigger a recursion error, then I would like to receive that bug
656+
# report!
617657
name_to_visit: t.Dict[str, int] = collections.defaultdict(int)
618-
# output ordering
619658
ordered_tasks: t.List[YoTask] = []
620659

621-
def visit(task_or_name: t.Union[YoTask, str]) -> None:
622-
if isinstance(task_or_name, str):
623-
name: str = task_or_name
624-
task: t.Optional[YoTask] = None
625-
else:
626-
name = task_or_name.name
627-
task = task_or_name
628-
if name_to_visit[name] == 2:
660+
def visit(task: YoTask) -> None:
661+
if name_to_visit[task.name] == 2:
629662
# already completed, skip
630663
return
631-
if name_to_visit[name] == 1:
664+
if name_to_visit[task.name] == 1:
632665
# currently visiting, not a DAG
633666
raise YoExc("Tasks express a circular dependency")
634667

635-
name_to_visit[name] = 1
636-
if not task:
637-
task = YoTask.load(name)
668+
name_to_visit[task.name] = 1
638669
for dep_name in task.dependencies:
639-
visit(dep_name)
640-
name_to_visit[name] = 2
670+
visit(name_to_task[dep_name])
671+
name_to_visit[task.name] = 2
641672
ordered_tasks.append(task)
642673

643-
for name in tasks:
644-
visit(name)
645-
646-
# Now ordered_tasks contains the order in which we should launch them.
647-
# Verify that there are no conflicts
648-
all_task_names = {t.name for t in ordered_tasks}
649-
for task in ordered_tasks:
650-
for conflict in task.conflicts:
651-
if conflict in all_task_names:
652-
raise YoExc(f"Task {task} conflicts with {conflict}")
674+
for task in name_to_task.values():
675+
visit(task)
653676

654-
# Do the thing!
677+
# Now ordered_tasks contains the order in which we should launch them. This
678+
# is just a nice-to-have: even if we launched them out of order, the
679+
# DEPENDS_ON function would enforce the order of execution. Regardless,
680+
# let's start the tasks.
655681
for task in ordered_tasks:
656682
_task_run(ctx, inst, task)
657683

0 commit comments

Comments
 (0)