stepup 3.2.0__tar.gz → 3.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stepup-3.2.0/stepup.egg-info → stepup-3.2.2}/PKG-INFO +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/__init__.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/__main__.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/actions.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/api.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/asyncio.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/browse.py +50 -12
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/call.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/cascade.py +3 -3
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/clean.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/deferred_glob.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/director.py +26 -2
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/enums.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/exceptions.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/file.py +2 -2
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/hash.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/interact.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/job.py +1 -1
- stepup-3.2.2/stepup/core/logo.svg +1 -0
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/nglob.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/pytest.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/render_jinja.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/reporter.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/rpc.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/runner.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/scheduler.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/script.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/startup.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/step.py +6 -4
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/stepinfo.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/tui.py +12 -2
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/utils.py +20 -5
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/watcher.py +1 -1
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/worker.py +227 -156
- {stepup-3.2.0 → stepup-3.2.2}/stepup/core/workflow.py +16 -1
- {stepup-3.2.0 → stepup-3.2.2/stepup.egg-info}/PKG-INFO +1 -1
- stepup-3.2.0/stepup/core/logo.svg +0 -1
- {stepup-3.2.0 → stepup-3.2.2}/LICENSE +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/MANIFEST.in +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/README.md +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/pyproject.toml +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/setup.cfg +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/SOURCES.txt +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/dependency_links.txt +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/entry_points.txt +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/requires.txt +0 -0
- {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -23,6 +23,7 @@ import argparse
|
|
|
23
23
|
import contextlib
|
|
24
24
|
import importlib.resources
|
|
25
25
|
import os
|
|
26
|
+
import pickle
|
|
26
27
|
import sqlite3
|
|
27
28
|
import stat
|
|
28
29
|
import traceback
|
|
@@ -366,8 +367,9 @@ class GraphServer(BaseHTTPRequestHandler):
|
|
|
366
367
|
|
|
367
368
|
# Format the state (if a file or a step)
|
|
368
369
|
if kind == "step":
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
sql_props = "SELECT state, pool, block, mandatory, dirty FROM step WHERE node = ?"
|
|
371
|
+
state_i, pool, block, mandatory_i, dirty = self.con.execute(
|
|
372
|
+
sql_props, (node_i,)
|
|
371
373
|
).fetchone()
|
|
372
374
|
state = StepState(state_i)
|
|
373
375
|
mandatory = Mandatory(mandatory_i)
|
|
@@ -379,6 +381,38 @@ class GraphServer(BaseHTTPRequestHandler):
|
|
|
379
381
|
yield '<p>Dirty: <span class="dirty">YES</span></p>'
|
|
380
382
|
else:
|
|
381
383
|
yield '<p>Dirty: <span class="clean">NO</span></p>'
|
|
384
|
+
if pool is not None:
|
|
385
|
+
yield f"<p><b>Pool:</b> {pool}</p>"
|
|
386
|
+
if block:
|
|
387
|
+
yield "<p><b>This step is blocked.</b></p>"
|
|
388
|
+
|
|
389
|
+
sql_env = "SELECT name, amended FROM env_var WHERE node = ?"
|
|
390
|
+
env_vars = list(self.con.execute(sql_env, (node_i,)))
|
|
391
|
+
if len(env_vars) > 0:
|
|
392
|
+
yield "<h3>Uses Environment Variables</h3><ul>"
|
|
393
|
+
for env_var, amended in env_vars:
|
|
394
|
+
yield (
|
|
395
|
+
f"<li>{env_var} <i>[amended]</i></li>" if amended else f"<li>{env_var}</li>"
|
|
396
|
+
)
|
|
397
|
+
yield "</ul>"
|
|
398
|
+
|
|
399
|
+
sql_ngm = "SELECT data FROM nglob_multi WHERE node = ?"
|
|
400
|
+
ngms = list(self.con.execute(sql_ngm, (node_i,)))
|
|
401
|
+
if len(ngms) > 0:
|
|
402
|
+
yield "<h3>Defines NGlob Multis</h3><ul>"
|
|
403
|
+
for row in ngms:
|
|
404
|
+
ngm = pickle.loads(row[0])
|
|
405
|
+
yield f"<li>{[ngs.pattern for ngs in ngm.nglob_singles]} {ngm.subs}</li>"
|
|
406
|
+
yield "</ul>"
|
|
407
|
+
|
|
408
|
+
sql_pooldefs = "SELECT name, size FROM pool_definition WHERE node = ?"
|
|
409
|
+
pooldefs = list(self.con.execute(sql_pooldefs, (node_i,)))
|
|
410
|
+
if len(pooldefs) > 0:
|
|
411
|
+
yield "<h3>Defines Pools</h3><ul>"
|
|
412
|
+
for pool, size in pooldefs:
|
|
413
|
+
yield f"<li>{pool} = {size}</li>"
|
|
414
|
+
yield "</ul>"
|
|
415
|
+
|
|
382
416
|
elif kind == "file":
|
|
383
417
|
(state_i, digest, mode, mtime, size, inode) = self.con.execute(
|
|
384
418
|
"SELECT state, digest, mode, mtime, size, inode FROM file WHERE node = ?", (node_i,)
|
|
@@ -427,10 +461,7 @@ class GraphServer(BaseHTTPRequestHandler):
|
|
|
427
461
|
"WHERE dependency.supplier = ? ORDER BY node.kind, node.label",
|
|
428
462
|
(node_i,),
|
|
429
463
|
):
|
|
430
|
-
|
|
431
|
-
if amended:
|
|
432
|
-
node_str += " <b>[amended]</b>"
|
|
433
|
-
yield node_str
|
|
464
|
+
yield self._format_node(cons_i, cons_kind, cons_label, False, state, amended)
|
|
434
465
|
yield "</table>"
|
|
435
466
|
|
|
436
467
|
# Format the suppliers
|
|
@@ -442,10 +473,7 @@ class GraphServer(BaseHTTPRequestHandler):
|
|
|
442
473
|
"WHERE dependency.consumer = ? ORDER BY node.kind, node.label",
|
|
443
474
|
(node_i,),
|
|
444
475
|
):
|
|
445
|
-
|
|
446
|
-
if amended:
|
|
447
|
-
node_str += " <b>[amended]</b>"
|
|
448
|
-
yield node_str
|
|
476
|
+
yield self._format_node(sup_i, sup_kind, sup_label, False, state, amended)
|
|
449
477
|
yield "</table>"
|
|
450
478
|
|
|
451
479
|
def _search(self, env, args):
|
|
@@ -494,14 +522,24 @@ class GraphServer(BaseHTTPRequestHandler):
|
|
|
494
522
|
# --- helpers ---
|
|
495
523
|
|
|
496
524
|
def _format_node(
|
|
497
|
-
self,
|
|
525
|
+
self,
|
|
526
|
+
i: int,
|
|
527
|
+
kind: str,
|
|
528
|
+
label: str,
|
|
529
|
+
orphan: bool,
|
|
530
|
+
state: int | None = None,
|
|
531
|
+
amended: bool = False,
|
|
498
532
|
) -> str:
|
|
499
533
|
sym = KIND_SYMBOLS.get(kind, f"?{kind}?")
|
|
500
534
|
node_str = f"{label}"
|
|
501
535
|
if i is not None:
|
|
502
536
|
node_str = f'<a href="/node/?i={i}">{node_str}</a>'
|
|
537
|
+
if len(label) == 0:
|
|
538
|
+
node_str = f"[{kind}]"
|
|
503
539
|
if orphan:
|
|
504
540
|
node_str = f"({node_str})"
|
|
541
|
+
if amended:
|
|
542
|
+
node_str += " <i>[amended]</i>"
|
|
505
543
|
if state is None:
|
|
506
544
|
state_str = ""
|
|
507
545
|
elif kind == "file":
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -398,7 +398,7 @@ class Node:
|
|
|
398
398
|
Parameters
|
|
399
399
|
----------
|
|
400
400
|
supplier
|
|
401
|
-
Other node that supplies to
|
|
401
|
+
Other node that supplies to this node.
|
|
402
402
|
|
|
403
403
|
Returns
|
|
404
404
|
-------
|
|
@@ -832,7 +832,7 @@ class Cascade:
|
|
|
832
832
|
while cleaned_some:
|
|
833
833
|
cleaned_some = False
|
|
834
834
|
# Look for orphans without consumers or products.
|
|
835
|
-
# As long
|
|
835
|
+
# As long nodes have consumers or products, they cannot be removed.
|
|
836
836
|
query = (
|
|
837
837
|
"SELECT i, kind, label FROM node WHERE orphan = TRUE AND "
|
|
838
838
|
"NOT EXISTS (SELECT 1 FROM node AS cnode WHERE node.i = cnode.creator) AND "
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -34,7 +34,10 @@ from importlib.metadata import version as get_version
|
|
|
34
34
|
import attrs
|
|
35
35
|
from path import Path
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
try:
|
|
38
|
+
import yappi
|
|
39
|
+
except ImportError:
|
|
40
|
+
yappi = None
|
|
38
41
|
|
|
39
42
|
from .asyncio import wait_for_events
|
|
40
43
|
from .enums import ReturnCode, StepState
|
|
@@ -46,6 +49,7 @@ from .rpc import allow_rpc, serve_socket_rpc
|
|
|
46
49
|
from .runner import Runner
|
|
47
50
|
from .scheduler import Scheduler
|
|
48
51
|
from .startup import startup_from_db
|
|
52
|
+
from .step import Step
|
|
49
53
|
from .stepinfo import StepInfo
|
|
50
54
|
from .utils import DBLock, check_plan, mynormpath
|
|
51
55
|
from .watcher import WATCHER_AVAILABLE, Watcher
|
|
@@ -70,6 +74,16 @@ async def async_main():
|
|
|
70
74
|
)
|
|
71
75
|
print(f"SOCKET {args.director_socket}", file=sys.stderr)
|
|
72
76
|
print(f"PID {os.getpid()}", file=sys.stderr)
|
|
77
|
+
print(f"LOG_LEVEL {args.log_level}", file=sys.stderr)
|
|
78
|
+
if args.yappi:
|
|
79
|
+
if yappi is None:
|
|
80
|
+
print(
|
|
81
|
+
"Yappi profiling requested, but the yappi module is not installed.",
|
|
82
|
+
file=sys.stderr,
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
yappi.set_clock_type("cpu")
|
|
86
|
+
yappi.start(builtins=True, profile_threads=True)
|
|
73
87
|
async with ReporterClient.socket(args.reporter_socket) as reporter:
|
|
74
88
|
num_workers = interpret_num_workers(args.num_workers)
|
|
75
89
|
await reporter.set_num_workers(num_workers)
|
|
@@ -94,6 +108,10 @@ async def async_main():
|
|
|
94
108
|
finally:
|
|
95
109
|
await reporter("DIRECTOR", "See you!")
|
|
96
110
|
await reporter.shutdown()
|
|
111
|
+
if args.yappi and yappi is not None:
|
|
112
|
+
yappi.stop()
|
|
113
|
+
stats = yappi.get_func_stats()
|
|
114
|
+
stats.save(".stepup/director.prof", type="pstat")
|
|
97
115
|
sys.exit(returncode.value)
|
|
98
116
|
|
|
99
117
|
|
|
@@ -169,6 +187,12 @@ def parse_args() -> argparse.Namespace:
|
|
|
169
187
|
action=argparse.BooleanOptionalAction,
|
|
170
188
|
help="Do not remove outdated output files.",
|
|
171
189
|
)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--yappi",
|
|
192
|
+
default=False,
|
|
193
|
+
action=argparse.BooleanOptionalAction,
|
|
194
|
+
help="Profile the director with Yappi (must be installed).",
|
|
195
|
+
)
|
|
172
196
|
args = parser.parse_args()
|
|
173
197
|
if WATCHER_AVAILABLE:
|
|
174
198
|
if args.watch_first:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -299,7 +299,7 @@ class File(Node):
|
|
|
299
299
|
# Local import to avoid cyclic imports.
|
|
300
300
|
from .step import Step # noqa: PLC0415
|
|
301
301
|
|
|
302
|
-
for step in self.consumers(Step):
|
|
302
|
+
for step in self.consumers(Step, include_orphans=True):
|
|
303
303
|
step.mark_pending()
|
|
304
304
|
elif state != FileState.OUTDATED:
|
|
305
305
|
raise ValueError(f"Cannot make file oudated when its state is {state}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 26 26"><path d="M9.5 0C7.5 0 6 1.6 6 3.5v3.4c0 .6-.3 1.1-.8 1.3A3.7 3.7 0 0 0 3 11.5v3.4c0 .6-.3 1.1-.7 1.3A3.6 3.6 0 0 0 0 19.5v3c0 2 1.6 3.5 3.5 3.5h19c2 0 3.5-1.6 3.5-3.5v-3c0-2-1.6-3.5-3.5-3.5-.8 0-1.5-.7-1.5-1.5v-3c0-2-1.6-3.5-3.5-3.5-.8 0-1.5-.7-1.5-1.5v-3c0-2-1.6-3.5-3.5-3.5Z"/><path id="a" fill="#fff" d="M12.5 8h-3C8.7 8 8 7.3 8 6.5v-3C8 2.7 8.7 2 9.5 2h3c.8 0 1.5.7 1.5 1.5v3c0 .8-.7 1.5-1.5 1.5Z"/><use xlink:href="#a" transform="translate(10 16)"/><use xlink:href="#a" transform="translate(2 16)"/><use xlink:href="#a" transform="translate(-6 16)"/><use xlink:href="#a" transform="translate(-3 8)"/><use xlink:href="#a" transform="translate(5 8)"/></svg>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -219,14 +219,16 @@ class Step(Node):
|
|
|
219
219
|
|
|
220
220
|
def format_properties(self) -> Iterator[tuple[str, str]]:
|
|
221
221
|
"""Iterate over key-value pairs that represent the properties of the node."""
|
|
222
|
-
state, pool, block, mandatory,
|
|
222
|
+
state, pool, block, mandatory, dirty, _ = self.properties()
|
|
223
223
|
yield "state", state.name
|
|
224
224
|
if mandatory != Mandatory.YES:
|
|
225
225
|
yield "mandatory", mandatory.name
|
|
226
226
|
if pool is not None:
|
|
227
227
|
yield "pool", pool
|
|
228
|
+
if dirty:
|
|
229
|
+
yield "dirty", "yes"
|
|
228
230
|
if block:
|
|
229
|
-
yield "
|
|
231
|
+
yield "blocked", "yes"
|
|
230
232
|
|
|
231
233
|
sql = "SELECT name, amended FROM env_var WHERE node = ?"
|
|
232
234
|
label = "env_var"
|
|
@@ -985,6 +987,6 @@ class Step(Node):
|
|
|
985
987
|
logger.info("Mark %s step PENDING: %s", state.name, self.label)
|
|
986
988
|
self.set_state(StepState.PENDING)
|
|
987
989
|
# First make all consumers (output files) pending
|
|
988
|
-
for file in self.consumers(File):
|
|
990
|
+
for file in self.consumers(File, include_orphans=True):
|
|
989
991
|
if file.get_state() == FileState.BUILT:
|
|
990
992
|
file.mark_outdated()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -123,6 +123,8 @@ async def async_boot(args: argparse.Namespace):
|
|
|
123
123
|
argv.append("--watch-first")
|
|
124
124
|
if not args.clean:
|
|
125
125
|
argv.append("--no-clean")
|
|
126
|
+
if args.yappi:
|
|
127
|
+
argv.append("--yappi")
|
|
126
128
|
returncode = 1 # Internal error unless it is overriden later by the director subprocess
|
|
127
129
|
try:
|
|
128
130
|
with open(".stepup/director.log", "w") as log_file:
|
|
@@ -299,7 +301,15 @@ def boot_subcommand(subparsers) -> callable:
|
|
|
299
301
|
default=os.getenv("STEPUP_PERF", None),
|
|
300
302
|
nargs="?",
|
|
301
303
|
const="500",
|
|
302
|
-
help="
|
|
304
|
+
help="Profile the director with perf, by default at a frequency of %(const)s Hz. "
|
|
303
305
|
"(Only supported on Linux with perf installed.)",
|
|
304
306
|
)
|
|
307
|
+
parser.add_argument(
|
|
308
|
+
"--yappi",
|
|
309
|
+
default=string_to_bool(os.getenv("STEPUP_YAPPI", "0")),
|
|
310
|
+
action=argparse.BooleanOptionalAction,
|
|
311
|
+
help="Profile the director with Yappi (must be installed). "
|
|
312
|
+
"This produces a .stepup/director.prof file that can be analyzed with "
|
|
313
|
+
"tools like SnakeViz.",
|
|
314
|
+
)
|
|
305
315
|
return boot_tool
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import asyncio
|
|
23
23
|
import contextlib
|
|
24
|
+
import logging
|
|
24
25
|
import os
|
|
25
26
|
import re
|
|
26
27
|
import shlex
|
|
@@ -37,6 +38,7 @@ __all__ = (
|
|
|
37
38
|
"DBLock",
|
|
38
39
|
"check_inp_path",
|
|
39
40
|
"check_plan",
|
|
41
|
+
"filter_dependencies",
|
|
40
42
|
"format_command",
|
|
41
43
|
"format_digest",
|
|
42
44
|
"get_local_import_paths",
|
|
@@ -44,6 +46,7 @@ __all__ = (
|
|
|
44
46
|
"myabsolute",
|
|
45
47
|
"mynormpath",
|
|
46
48
|
"myparent",
|
|
49
|
+
"myrealpath",
|
|
47
50
|
"myrelpath",
|
|
48
51
|
"remove_path",
|
|
49
52
|
"string_to_bool",
|
|
@@ -53,6 +56,9 @@ __all__ = (
|
|
|
53
56
|
)
|
|
54
57
|
|
|
55
58
|
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
56
62
|
#
|
|
57
63
|
# Custom path operations
|
|
58
64
|
#
|
|
@@ -66,6 +72,14 @@ def mynormpath(path: str) -> Path:
|
|
|
66
72
|
return result
|
|
67
73
|
|
|
68
74
|
|
|
75
|
+
def myrealpath(path: str) -> Path:
|
|
76
|
+
"""Like Path.realpath path but keep the trailing separator"""
|
|
77
|
+
result = Path(path).realpath()
|
|
78
|
+
if path.endswith(os.sep) and not result.endswith("/"):
|
|
79
|
+
result = result / ""
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
69
83
|
def myrelpath(path: str, start: str = ".") -> Path:
|
|
70
84
|
"""Like Path.relpath path but keep the trailing separator"""
|
|
71
85
|
result = Path(path).relpath(start)
|
|
@@ -257,20 +271,21 @@ def filter_dependencies(paths: Collection[str]) -> set[Path]:
|
|
|
257
271
|
raise ValueError(f"Invalid filter item: {filter_item}")
|
|
258
272
|
prefix = filter_item[1:]
|
|
259
273
|
if not prefix.startswith("/"):
|
|
260
|
-
prefix =
|
|
274
|
+
prefix = myrealpath(stepup_root / prefix)
|
|
261
275
|
rules.append((prefix, keep))
|
|
262
276
|
|
|
263
277
|
# Filter paths according to the rules.
|
|
264
278
|
result = set()
|
|
279
|
+
realpwd = myrealpath(os.getcwd())
|
|
265
280
|
for path in paths:
|
|
266
|
-
abspath =
|
|
281
|
+
abspath = myrealpath(path)
|
|
267
282
|
for prefix, keep in rules:
|
|
268
283
|
if abspath.startswith(prefix):
|
|
269
284
|
if keep:
|
|
270
|
-
result.add(myrelpath(
|
|
285
|
+
result.add(myrelpath(abspath, realpwd))
|
|
271
286
|
break
|
|
272
287
|
else:
|
|
273
|
-
raise AssertionError("No matching rule found for path: {path}")
|
|
288
|
+
raise AssertionError(f"No matching rule found for path: {path}")
|
|
274
289
|
return result
|
|
275
290
|
|
|
276
291
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -25,6 +25,8 @@ import contextlib
|
|
|
25
25
|
import ctypes
|
|
26
26
|
import inspect
|
|
27
27
|
import io
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
28
30
|
import os
|
|
29
31
|
import queue
|
|
30
32
|
import resource
|
|
@@ -48,7 +50,7 @@ from .reporter import ReporterClient
|
|
|
48
50
|
from .rpc import AsyncRPCClient, allow_rpc, serve_socket_rpc
|
|
49
51
|
from .scheduler import Scheduler
|
|
50
52
|
from .step import Step, split_step_label
|
|
51
|
-
from .utils import DBLock
|
|
53
|
+
from .utils import DBLock, string_to_bool
|
|
52
54
|
from .workflow import Workflow
|
|
53
55
|
|
|
54
56
|
__all__ = ("WorkThread", "WorkerClient", "WorkerHandler", "WorkerStep")
|
|
@@ -59,6 +61,9 @@ __all__ = ("WorkThread", "WorkerClient", "WorkerHandler", "WorkerStep")
|
|
|
59
61
|
#
|
|
60
62
|
|
|
61
63
|
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
|
|
62
67
|
@attrs.define
|
|
63
68
|
class WorkerClient:
|
|
64
69
|
"""Client interface to a worker, used by the director process."""
|
|
@@ -109,6 +114,7 @@ class WorkerClient:
|
|
|
109
114
|
self.director_socket_path,
|
|
110
115
|
worker_socket_path,
|
|
111
116
|
str(self.idx),
|
|
117
|
+
f"--log-level={logging.getLevelName(logging.root.level)}",
|
|
112
118
|
]
|
|
113
119
|
if self.show_perf:
|
|
114
120
|
argv.append("--show-perf")
|
|
@@ -223,8 +229,6 @@ class WorkerClient:
|
|
|
223
229
|
return True
|
|
224
230
|
|
|
225
231
|
# Delegate the calculation of the output part of the step hash to the worker.
|
|
226
|
-
# With skipping=True, the worker knows the outputs should not have changed
|
|
227
|
-
# and will report it on screen.
|
|
228
232
|
new_step_hash, new_out_hashes = await self.compute_out_step_hash(step, new_step_hash)
|
|
229
233
|
|
|
230
234
|
if step_hash.out_digest != new_step_hash.out_digest:
|
|
@@ -516,10 +520,18 @@ class WorkerStep:
|
|
|
516
520
|
|
|
517
521
|
|
|
518
522
|
PYCODE_WRAPPER = """\
|
|
523
|
+
import logging
|
|
524
|
+
import os
|
|
519
525
|
import sys
|
|
520
526
|
import runpy
|
|
527
|
+
from path import Path
|
|
521
528
|
from stepup.core.api import amend
|
|
522
529
|
from stepup.core.utils import get_local_import_paths
|
|
530
|
+
logging.basicConfig(
|
|
531
|
+
format="%(asctime)s %(levelname)8s %(name)24s :: %(message)s",
|
|
532
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
533
|
+
level=logging.{log_level},
|
|
534
|
+
)
|
|
523
535
|
sys.argv = {argv}
|
|
524
536
|
try:
|
|
525
537
|
runpy.run_path({script}, run_name="__main__")
|
|
@@ -645,7 +657,11 @@ class WorkThread(threading.Thread):
|
|
|
645
657
|
# Run the script
|
|
646
658
|
return self.runsh_verbose(
|
|
647
659
|
f"{sys.executable} -",
|
|
648
|
-
PYCODE_WRAPPER.format(
|
|
660
|
+
PYCODE_WRAPPER.format(
|
|
661
|
+
argv=repr([script, *args]),
|
|
662
|
+
script=repr(script),
|
|
663
|
+
log_level=logging.getLevelName(logging.root.level),
|
|
664
|
+
),
|
|
649
665
|
)
|
|
650
666
|
|
|
651
667
|
|
|
@@ -690,6 +706,38 @@ def check_executable(executable: Path, shebang: str | None = None) -> bool:
|
|
|
690
706
|
return True
|
|
691
707
|
|
|
692
708
|
|
|
709
|
+
@attrs.define
|
|
710
|
+
class Timer:
|
|
711
|
+
"""Context managers to construct a JSON string with times spent in typical worker tasks."""
|
|
712
|
+
|
|
713
|
+
start: float = attrs.field(init=False, factory=perf_counter)
|
|
714
|
+
sections: dict[str, float] = attrs.field(init=False, factory=dict)
|
|
715
|
+
|
|
716
|
+
@contextlib.contextmanager
|
|
717
|
+
def section(self, section: str):
|
|
718
|
+
"""Context manager for a timed section.
|
|
719
|
+
|
|
720
|
+
Parameters
|
|
721
|
+
----------
|
|
722
|
+
section
|
|
723
|
+
The name of the section.
|
|
724
|
+
"""
|
|
725
|
+
time = perf_counter()
|
|
726
|
+
try:
|
|
727
|
+
yield
|
|
728
|
+
finally:
|
|
729
|
+
time = perf_counter() - time
|
|
730
|
+
self.sections[section] = self.sections.get(section, 0.0) + time
|
|
731
|
+
|
|
732
|
+
def report(self):
|
|
733
|
+
"""Write the timings line to standard error."""
|
|
734
|
+
time = perf_counter() - self.start
|
|
735
|
+
sections = self.sections.copy()
|
|
736
|
+
sections["idle/other"] = time - sum(sections.values())
|
|
737
|
+
sections["total"] = time
|
|
738
|
+
print(f"TIMINGS: {json.dumps(sections)}", file=sys.stderr)
|
|
739
|
+
|
|
740
|
+
|
|
693
741
|
@attrs.define
|
|
694
742
|
class WorkerHandler:
|
|
695
743
|
"""RPC Handler in the worker process to respond to requests from the WorkerClient."""
|
|
@@ -699,6 +747,7 @@ class WorkerHandler:
|
|
|
699
747
|
explain_rerun: bool = attrs.field()
|
|
700
748
|
stop_event: asyncio.Event = attrs.field(factory=asyncio.Event)
|
|
701
749
|
step: WorkerStep | None = attrs.field(init=False, default=None)
|
|
750
|
+
timer: Timer = attrs.field(init=False, factory=Timer)
|
|
702
751
|
|
|
703
752
|
@allow_rpc
|
|
704
753
|
def shutdown(self):
|
|
@@ -760,18 +809,19 @@ class WorkerHandler:
|
|
|
760
809
|
The new hash of the step, with the input part already computed, if available.
|
|
761
810
|
`None` is yielded if, unexpectedly, some inputs are missing or have changed.
|
|
762
811
|
"""
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
812
|
+
with self.timer.section("new_step"):
|
|
813
|
+
if self.step is not None:
|
|
814
|
+
raise RPCError(
|
|
815
|
+
"Worker cannot initiate two steps at the same time. "
|
|
816
|
+
f"Still working on {self.step.action}"
|
|
817
|
+
)
|
|
768
818
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
819
|
+
# Create the step
|
|
820
|
+
action, workdir = split_step_label(label)
|
|
821
|
+
self.step = WorkerStep(i, action, workdir)
|
|
772
822
|
|
|
773
|
-
|
|
774
|
-
|
|
823
|
+
# Create initial StepHash
|
|
824
|
+
return self.compute_inp_step_hash(inp_hashes, env_vars, check_hash)[0]
|
|
775
825
|
|
|
776
826
|
def compute_inp_step_hash(
|
|
777
827
|
self,
|
|
@@ -802,41 +852,44 @@ class WorkerHandler:
|
|
|
802
852
|
so they can be updated in StepUp's workflow database if desired.
|
|
803
853
|
Unchanged ones are not included.
|
|
804
854
|
"""
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if new_file_hash
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
855
|
+
with self.timer.section("compute_inp_step_hash"):
|
|
856
|
+
# Check the input hashes
|
|
857
|
+
messages = []
|
|
858
|
+
new_inp_hashes = []
|
|
859
|
+
all_inp_hashes = []
|
|
860
|
+
for path, old_file_hash in old_inp_hashes:
|
|
861
|
+
new_file_hash = old_file_hash.regen(path)
|
|
862
|
+
all_inp_hashes.append((path, new_file_hash))
|
|
863
|
+
if check_hash and new_file_hash != old_file_hash:
|
|
864
|
+
if new_file_hash.is_unknown:
|
|
865
|
+
messages.append(f"Input vanished unexpectedly: {path} ")
|
|
866
|
+
else:
|
|
867
|
+
messages.append(
|
|
868
|
+
f"Input changed unexpectedly: {path} "
|
|
869
|
+
+ fmt_file_hash_diff(old_file_hash, new_file_hash)
|
|
870
|
+
)
|
|
871
|
+
new_inp_hashes.append((path, new_file_hash))
|
|
872
|
+
|
|
873
|
+
# If there are unexpected issues with inputs, bail out.
|
|
874
|
+
if len(messages) > 0:
|
|
875
|
+
self.step.inp_messages = messages
|
|
876
|
+
self.step.success = False
|
|
877
|
+
return None, new_inp_hashes
|
|
827
878
|
|
|
828
|
-
|
|
829
|
-
|
|
879
|
+
# Add the environment variables to the hash
|
|
880
|
+
env_var_values = [(env_var, os.environ.get(env_var)) for env_var in env_vars]
|
|
830
881
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
882
|
+
# Create the StepHash
|
|
883
|
+
label = self.step.action
|
|
884
|
+
if self.step.workdir != "./":
|
|
885
|
+
label += f" # wd={self.step.workdir}"
|
|
886
|
+
result = StepHash.from_inp(
|
|
887
|
+
f"{label}", self.explain_rerun, all_inp_hashes, env_var_values
|
|
888
|
+
)
|
|
836
889
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
890
|
+
# Copy the inp_digest, because it can be useful for some actions.
|
|
891
|
+
self.step.inp_digest = result.inp_digest
|
|
892
|
+
return result, []
|
|
840
893
|
|
|
841
894
|
@allow_rpc
|
|
842
895
|
def compute_out_step_hash(
|
|
@@ -864,22 +917,23 @@ class WorkerHandler:
|
|
|
864
917
|
so they can be updated in StepUp's workflow database if desired.
|
|
865
918
|
Unchanged ones are not included.
|
|
866
919
|
"""
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
920
|
+
with self.timer.section("compute_out_step_hash"):
|
|
921
|
+
# Check the output hashes
|
|
922
|
+
new_out_hashes = []
|
|
923
|
+
all_out_hashes = []
|
|
924
|
+
for path, old_file_hash in sorted(old_out_hashes):
|
|
925
|
+
new_file_hash = old_file_hash.regen(path)
|
|
926
|
+
all_out_hashes.append((path, new_file_hash))
|
|
927
|
+
if new_file_hash != old_file_hash:
|
|
928
|
+
new_out_hashes.append((path, new_file_hash))
|
|
929
|
+
if new_file_hash.is_unknown:
|
|
930
|
+
self.step.out_missing.append(path)
|
|
931
|
+
self.step.success = False
|
|
878
932
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
933
|
+
# Update the step hash
|
|
934
|
+
if step_hash is not None:
|
|
935
|
+
step_hash = step_hash.evolve_out(all_out_hashes)
|
|
936
|
+
return step_hash, new_out_hashes
|
|
883
937
|
|
|
884
938
|
@allow_rpc
|
|
885
939
|
def compute_full_step_hash(
|
|
@@ -939,57 +993,58 @@ class WorkerHandler:
|
|
|
939
993
|
|
|
940
994
|
@allow_rpc
|
|
941
995
|
async def run(self):
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
996
|
+
with self.timer.section("run"):
|
|
997
|
+
await self.reporter("START", self.step.description)
|
|
998
|
+
await self.reporter.start_step(self.step.description, self.step.i)
|
|
999
|
+
|
|
1000
|
+
# For internal use in actions:
|
|
1001
|
+
os.environ["STEPUP_STEP_I"] = str(self.step.i)
|
|
1002
|
+
# Client code may use the following:
|
|
1003
|
+
os.environ["STEPUP_STEP_INP_DIGEST"] = self.step.inp_digest.hex()
|
|
1004
|
+
os.environ["ROOT"] = str(Path.cwd().relpath(self.step.workdir))
|
|
1005
|
+
os.environ["HERE"] = str(self.step.workdir.relpath())
|
|
1006
|
+
# Note: the variables defined here should be listed in stepup.core.api.getenv
|
|
1007
|
+
|
|
1008
|
+
# Create IO redirection for stdout and stderr
|
|
1009
|
+
step_err = io.StringIO()
|
|
1010
|
+
step_out = io.StringIO()
|
|
1011
|
+
with (
|
|
1012
|
+
contextlib.chdir(self.step.workdir),
|
|
1013
|
+
contextlib.redirect_stderr(step_err),
|
|
1014
|
+
contextlib.redirect_stdout(step_out),
|
|
1015
|
+
):
|
|
1016
|
+
if self.show_perf:
|
|
1017
|
+
ru_initial = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
1018
|
+
pt_initial = perf_counter()
|
|
1019
|
+
self.step.thread = WorkThread(self.step.action)
|
|
1020
|
+
self.step.thread.start()
|
|
1021
|
+
await self.step.thread.done.wait()
|
|
1022
|
+
self.step.thread.join()
|
|
1023
|
+
self.step.returncode = self.step.thread.returncode
|
|
1024
|
+
self.step.thread = None
|
|
1025
|
+
self.step.stdout = step_out.getvalue()
|
|
1026
|
+
self.step.stderr = step_err.getvalue()
|
|
1027
|
+
|
|
1028
|
+
# Clean up environment variables (to avoid potential confusion)
|
|
1029
|
+
del os.environ["STEPUP_STEP_I"]
|
|
1030
|
+
del os.environ["STEPUP_STEP_INP_DIGEST"]
|
|
1031
|
+
del os.environ["ROOT"]
|
|
1032
|
+
del os.environ["HERE"]
|
|
1033
|
+
|
|
1034
|
+
# Process results of the step.
|
|
961
1035
|
if self.show_perf:
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
del os.environ["STEPUP_STEP_I"]
|
|
975
|
-
del os.environ["STEPUP_STEP_INP_DIGEST"]
|
|
976
|
-
del os.environ["ROOT"]
|
|
977
|
-
del os.environ["HERE"]
|
|
978
|
-
|
|
979
|
-
# Process results of the step.
|
|
980
|
-
if self.show_perf:
|
|
981
|
-
ru_final = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
982
|
-
utime = ru_final.ru_utime - ru_initial.ru_utime
|
|
983
|
-
stime = ru_final.ru_stime - ru_initial.ru_stime
|
|
984
|
-
wtime = perf_counter() - pt_initial
|
|
985
|
-
ru_lines = [
|
|
986
|
-
f"User CPU time [s]: {utime:9.4f}",
|
|
987
|
-
f"System CPU time [s]: {stime:9.4f}",
|
|
988
|
-
f"Total CPU time [s]: {utime + stime:9.4f}",
|
|
989
|
-
f"Wall time [s]: {wtime:9.4f}",
|
|
990
|
-
]
|
|
991
|
-
self.step.perf_info = "\n".join(ru_lines)
|
|
992
|
-
self.step.success = self.step.returncode == 0
|
|
1036
|
+
ru_final = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
1037
|
+
utime = ru_final.ru_utime - ru_initial.ru_utime
|
|
1038
|
+
stime = ru_final.ru_stime - ru_initial.ru_stime
|
|
1039
|
+
wtime = perf_counter() - pt_initial
|
|
1040
|
+
ru_lines = [
|
|
1041
|
+
f"User CPU time [s]: {utime:9.4f}",
|
|
1042
|
+
f"System CPU time [s]: {stime:9.4f}",
|
|
1043
|
+
f"Total CPU time [s]: {utime + stime:9.4f}",
|
|
1044
|
+
f"Wall time [s]: {wtime:9.4f}",
|
|
1045
|
+
]
|
|
1046
|
+
self.step.perf_info = "\n".join(ru_lines)
|
|
1047
|
+
self.step.success = self.step.returncode == 0
|
|
993
1048
|
|
|
994
1049
|
@allow_rpc
|
|
995
1050
|
def get_success(self) -> bool:
|
|
@@ -1004,51 +1059,52 @@ class WorkerHandler:
|
|
|
1004
1059
|
|
|
1005
1060
|
@allow_rpc
|
|
1006
1061
|
async def report(self):
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
(
|
|
1026
|
-
|
|
1027
|
-
|
|
1062
|
+
with self.timer.section("report"):
|
|
1063
|
+
pages = []
|
|
1064
|
+
if not self.step.success:
|
|
1065
|
+
# Format command such that it can be copied and pasted into a shell.
|
|
1066
|
+
command = "stepup act "
|
|
1067
|
+
if any(word.startswith("-") for word in shlex.split(self.step.action)):
|
|
1068
|
+
command += "-- "
|
|
1069
|
+
command += self.step.action
|
|
1070
|
+
if self.step.workdir != "./":
|
|
1071
|
+
command = f"(cd {self.step.workdir} && {command})"
|
|
1072
|
+
lines = [f"Command {command}"]
|
|
1073
|
+
# Other info on the execution of the step
|
|
1074
|
+
if self.step.returncode is not None:
|
|
1075
|
+
lines.append(f"Return code {self.step.returncode}")
|
|
1076
|
+
pages.append(("Step info", "\n".join(lines)))
|
|
1077
|
+
if len(self.step.perf_info) > 0:
|
|
1078
|
+
pages.append(("Performance details", self.step.perf_info))
|
|
1079
|
+
if self.step.rescheduled_info != "":
|
|
1080
|
+
pages.append(
|
|
1081
|
+
(
|
|
1082
|
+
"Rescheduling due to unavailable amended inputs",
|
|
1083
|
+
self.step.rescheduled_info,
|
|
1084
|
+
)
|
|
1028
1085
|
)
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
self.step = None
|
|
1086
|
+
else:
|
|
1087
|
+
if len(self.step.inp_messages) > 0:
|
|
1088
|
+
self.step.inp_messages.sort()
|
|
1089
|
+
pages.append(("Invalid inputs", "\n".join(self.step.inp_messages)))
|
|
1090
|
+
if len(self.step.out_missing) > 0:
|
|
1091
|
+
self.step.out_missing.sort()
|
|
1092
|
+
pages.append(("Expected outputs not created", "\n".join(self.step.out_missing)))
|
|
1093
|
+
stdout = self.step.stdout.rstrip()
|
|
1094
|
+
if len(stdout) > 0:
|
|
1095
|
+
pages.append(("Standard output", stdout))
|
|
1096
|
+
stderr = self.step.stderr.rstrip()
|
|
1097
|
+
if len(stderr) > 0:
|
|
1098
|
+
pages.append(("Standard error", stderr))
|
|
1099
|
+
if self.step.rescheduled_info != "":
|
|
1100
|
+
action = "RESCHEDULE"
|
|
1101
|
+
elif self.step.success:
|
|
1102
|
+
action = "SUCCESS"
|
|
1103
|
+
else:
|
|
1104
|
+
action = "FAIL"
|
|
1105
|
+
await self.reporter.stop_step(self.step.i)
|
|
1106
|
+
await self.reporter(action, self.step.description, pages)
|
|
1107
|
+
self.step = None
|
|
1052
1108
|
|
|
1053
1109
|
@allow_rpc
|
|
1054
1110
|
async def skip(self, step_hash: StepHash):
|
|
@@ -1090,6 +1146,14 @@ def parse_args():
|
|
|
1090
1146
|
parser.add_argument("director_socket", type=Path, help="Socket of the director")
|
|
1091
1147
|
parser.add_argument("worker_socket", type=Path, help="Socket of the worker (to be created)")
|
|
1092
1148
|
parser.add_argument("worker_idx", type=int, help="Worker index")
|
|
1149
|
+
debug = string_to_bool(os.getenv("STEPUP_DEBUG", "0"))
|
|
1150
|
+
parser.add_argument(
|
|
1151
|
+
"--log-level",
|
|
1152
|
+
"-l",
|
|
1153
|
+
default=os.getenv("STEPUP_LOG_LEVEL", "DEBUG" if debug else "WARNING").upper(),
|
|
1154
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
1155
|
+
help="Set the logging level. [default=%(default)s]",
|
|
1156
|
+
)
|
|
1093
1157
|
parser.add_argument(
|
|
1094
1158
|
"--reporter",
|
|
1095
1159
|
"-r",
|
|
@@ -1117,13 +1181,20 @@ def parse_args():
|
|
|
1117
1181
|
|
|
1118
1182
|
async def async_main():
|
|
1119
1183
|
args = parse_args()
|
|
1184
|
+
logging.basicConfig(
|
|
1185
|
+
format="%(asctime)s %(levelname)8s %(name)24s :: %(message)s",
|
|
1186
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
1187
|
+
level=args.log_level,
|
|
1188
|
+
)
|
|
1120
1189
|
os.environ["STEPUP_DIRECTOR_SOCKET"] = args.director_socket
|
|
1121
1190
|
os.environ["STEPUP_ROOT"] = str(Path.cwd())
|
|
1122
1191
|
print(f"PID {os.getpid()}", file=sys.stderr)
|
|
1192
|
+
print(f"LOG_LEVEL {args.log_level}", file=sys.stderr)
|
|
1123
1193
|
async with ReporterClient.socket(args.reporter_socket) as reporter:
|
|
1124
1194
|
# Create the worker handler for the RPC server.
|
|
1125
1195
|
handler = WorkerHandler(reporter, args.show_perf, args.explain_rerun)
|
|
1126
1196
|
await serve_socket_rpc(handler, args.worker_socket, handler.stop_event)
|
|
1197
|
+
handler.timer.report()
|
|
1127
1198
|
|
|
1128
1199
|
|
|
1129
1200
|
def main():
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
-
#
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
3
|
#
|
|
4
4
|
# This file is part of StepUp Core.
|
|
5
5
|
#
|
|
@@ -1118,6 +1118,21 @@ class Workflow(Cascade):
|
|
|
1118
1118
|
return self.declare_missing(dg, matching_paths)
|
|
1119
1119
|
|
|
1120
1120
|
def clean(self):
|
|
1121
|
+
# Search for parent directories of STEPUP_ROOT that are no longer used.
|
|
1122
|
+
# The are not cleaned up like other unused directories,
|
|
1123
|
+
# because they are treated as "always static" with the root node as creator.
|
|
1124
|
+
parent_stack = []
|
|
1125
|
+
iparent = 1
|
|
1126
|
+
while True:
|
|
1127
|
+
node = self.find(File, "../" * iparent)
|
|
1128
|
+
if node is None:
|
|
1129
|
+
break
|
|
1130
|
+
parent_stack.insert(0, node)
|
|
1131
|
+
iparent += 1
|
|
1132
|
+
for node in parent_stack:
|
|
1133
|
+
if any(node.consumers()):
|
|
1134
|
+
break
|
|
1135
|
+
node.orphan()
|
|
1121
1136
|
# Get rid of deferred glob files that are no longer used.
|
|
1122
1137
|
for dg in self.nodes(DeferredGlob):
|
|
1123
1138
|
files = sorted(dg.products(), reverse=True, key=(lambda node: node.path))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="26" height="26"><g transform="translate(-5)"><path d="M14.5 0c-2 0-3.5 1.6-3.5 3.5v3.4c0 .6-.3 1.1-.8 1.3A3.7 3.7 0 0 0 8 11.5v3.4c0 .6-.3 1.1-.7 1.3A3.6 3.6 0 0 0 5 19.5v3c0 2 1.6 3.5 3.5 3.5h19c2 0 3.5-1.6 3.5-3.5v-3c0-2-1.6-3.5-3.5-3.5-.8 0-1.5-.7-1.5-1.5v-3c0-2-1.6-3.5-3.5-3.5-.8 0-1.5-.7-1.5-1.5v-3c0-2-1.6-3.5-3.5-3.5Z" overflow="visible" paint-order="stroke markers fill"/><path id="a" fill="#fff" d="M17.5 8h-3c-.8 0-1.5-.7-1.5-1.5v-3c0-.8.7-1.5 1.5-1.5h3c.8 0 1.5.7 1.5 1.5v3c0 .8-.7 1.5-1.5 1.5z" baseline-shift="baseline" display="inline" overflow="visible" paint-order="stroke markers fill" stop-color="#000" vector-effect="none"/><use xlink:href="#a" transform="translate(10 16)"/><use xlink:href="#a" transform="translate(2 16)"/><use xlink:href="#a" transform="translate(-6 16)"/><use xlink:href="#a" transform="translate(-3 8)"/><use xlink:href="#a" transform="translate(5 8)"/></g></svg>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|