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.
Files changed (47) hide show
  1. {stepup-3.2.0/stepup.egg-info → stepup-3.2.2}/PKG-INFO +1 -1
  2. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/__init__.py +1 -1
  3. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/__main__.py +1 -1
  4. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/actions.py +1 -1
  5. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/api.py +1 -1
  6. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/asyncio.py +1 -1
  7. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/browse.py +50 -12
  8. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/call.py +1 -1
  9. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/cascade.py +3 -3
  10. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/clean.py +1 -1
  11. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/deferred_glob.py +1 -1
  12. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/director.py +26 -2
  13. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/enums.py +1 -1
  14. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/exceptions.py +1 -1
  15. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/file.py +2 -2
  16. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/hash.py +1 -1
  17. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/interact.py +1 -1
  18. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/job.py +1 -1
  19. stepup-3.2.2/stepup/core/logo.svg +1 -0
  20. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/nglob.py +1 -1
  21. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/pytest.py +1 -1
  22. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/render_jinja.py +1 -1
  23. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/reporter.py +1 -1
  24. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/rpc.py +1 -1
  25. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/runner.py +1 -1
  26. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/scheduler.py +1 -1
  27. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/script.py +1 -1
  28. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/startup.py +1 -1
  29. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/step.py +6 -4
  30. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/stepinfo.py +1 -1
  31. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/tui.py +12 -2
  32. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/utils.py +20 -5
  33. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/watcher.py +1 -1
  34. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/worker.py +227 -156
  35. {stepup-3.2.0 → stepup-3.2.2}/stepup/core/workflow.py +16 -1
  36. {stepup-3.2.0 → stepup-3.2.2/stepup.egg-info}/PKG-INFO +1 -1
  37. stepup-3.2.0/stepup/core/logo.svg +0 -1
  38. {stepup-3.2.0 → stepup-3.2.2}/LICENSE +0 -0
  39. {stepup-3.2.0 → stepup-3.2.2}/MANIFEST.in +0 -0
  40. {stepup-3.2.0 → stepup-3.2.2}/README.md +0 -0
  41. {stepup-3.2.0 → stepup-3.2.2}/pyproject.toml +0 -0
  42. {stepup-3.2.0 → stepup-3.2.2}/setup.cfg +0 -0
  43. {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/SOURCES.txt +0 -0
  44. {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/dependency_links.txt +0 -0
  45. {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/entry_points.txt +0 -0
  46. {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/requires.txt +0 -0
  47. {stepup-3.2.0 → stepup-3.2.2}/stepup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stepup
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: StepUp Core provides the basic framework for the StepUp build tool
5
5
  Author-email: Toon Verstraelen <toon.verstraelen@ugent.be>
6
6
  License-Expression: GPL-3.0-or-later
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
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
- (state_i, mandatory_i, dirty) = self.con.execute(
370
- "SELECT state, mandatory, dirty FROM step WHERE node = ?", (node_i,)
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
- node_str = self._format_node(cons_i, cons_kind, cons_label, False, state)
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
- node_str = self._format_node(sup_i, sup_kind, sup_label, False, state)
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, i: int, kind: str, label: str, orphan: bool, state: int | None = None
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
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
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 thise node.
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 is nodes have consumers or products, they cannot be removed.
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
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
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
- from stepup.core.step import Step
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
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
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}")
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -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
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Script extends StepUp with scripts that combine planning and running.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Script.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
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, _, _ = self.properties()
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 "block", block
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
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
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="Run the director under perf record, by default at a frequency of 500 Hz. "
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
- # © 2024–2025 Toon Verstraelen
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 = mynormpath(stepup_root / 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 = myabsolute(path)
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(path))
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
- # © 2024–2025 Toon Verstraelen
2
+ # Copyright 2024-2026 Toon Verstraelen
3
3
  #
4
4
  # This file is part of StepUp Core.
5
5
  #
@@ -1,5 +1,5 @@
1
1
  # StepUp Core provides the basic framework for the StepUp build tool.
2
- # © 2024–2025 Toon Verstraelen
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(argv=repr([script, *args]), script=repr(script)),
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
- if self.step is not None:
764
- raise RPCError(
765
- "Worker cannot initiate two steps at the same time. "
766
- f"Still working on {self.step.action}"
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
- # Create the step
770
- action, workdir = split_step_label(label)
771
- self.step = WorkerStep(i, action, workdir)
819
+ # Create the step
820
+ action, workdir = split_step_label(label)
821
+ self.step = WorkerStep(i, action, workdir)
772
822
 
773
- # Create initial StepHash
774
- return self.compute_inp_step_hash(inp_hashes, env_vars, check_hash)[0]
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
- # Check the input hashes
806
- messages = []
807
- new_inp_hashes = []
808
- all_inp_hashes = []
809
- for path, old_file_hash in old_inp_hashes:
810
- new_file_hash = old_file_hash.regen(path)
811
- all_inp_hashes.append((path, new_file_hash))
812
- if check_hash and new_file_hash != old_file_hash:
813
- if new_file_hash.is_unknown:
814
- messages.append(f"Input vanished unexpectedly: {path} ")
815
- else:
816
- messages.append(
817
- f"Input changed unexpectedly: {path} "
818
- + fmt_file_hash_diff(old_file_hash, new_file_hash)
819
- )
820
- new_inp_hashes.append((path, new_file_hash))
821
-
822
- # If there are unexpected issues with inputs, bail out.
823
- if len(messages) > 0:
824
- self.step.inp_messages = messages
825
- self.step.success = False
826
- return None, new_inp_hashes
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
- # Add the environment variables to the hash
829
- env_var_values = [(env_var, os.environ.get(env_var)) for env_var in env_vars]
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
- # Create the StepHash
832
- label = self.step.action
833
- if self.step.workdir != "./":
834
- label += f" # wd={self.step.workdir}"
835
- result = StepHash.from_inp(f"{label}", self.explain_rerun, all_inp_hashes, env_var_values)
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
- # Copy the inp_digest, because it can be useful for some actions.
838
- self.step.inp_digest = result.inp_digest
839
- return result, []
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
- # Check the output hashes
868
- new_out_hashes = []
869
- all_out_hashes = []
870
- for path, old_file_hash in sorted(old_out_hashes):
871
- new_file_hash = old_file_hash.regen(path)
872
- all_out_hashes.append((path, new_file_hash))
873
- if new_file_hash != old_file_hash:
874
- new_out_hashes.append((path, new_file_hash))
875
- if new_file_hash.is_unknown:
876
- self.step.out_missing.append(path)
877
- self.step.success = False
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
- # Update the step hash
880
- if step_hash is not None:
881
- step_hash = step_hash.evolve_out(all_out_hashes)
882
- return step_hash, new_out_hashes
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
- await self.reporter("START", self.step.description)
943
- await self.reporter.start_step(self.step.description, self.step.i)
944
-
945
- # For internal use in actions:
946
- os.environ["STEPUP_STEP_I"] = str(self.step.i)
947
- # Client code may use the following:
948
- os.environ["STEPUP_STEP_INP_DIGEST"] = self.step.inp_digest.hex()
949
- os.environ["ROOT"] = str(Path.cwd().relpath(self.step.workdir))
950
- os.environ["HERE"] = str(self.step.workdir.relpath())
951
- # Note: the variables defined here should be listed in stepup.core.api.getenv
952
-
953
- # Create IO redirection for stdout and stderr
954
- step_err = io.StringIO()
955
- step_out = io.StringIO()
956
- with (
957
- contextlib.chdir(self.step.workdir),
958
- contextlib.redirect_stderr(step_err),
959
- contextlib.redirect_stdout(step_out),
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
- ru_initial = resource.getrusage(resource.RUSAGE_CHILDREN)
963
- pt_initial = perf_counter()
964
- self.step.thread = WorkThread(self.step.action)
965
- self.step.thread.start()
966
- await self.step.thread.done.wait()
967
- self.step.thread.join()
968
- self.step.returncode = self.step.thread.returncode
969
- self.step.thread = None
970
- self.step.stdout = step_out.getvalue()
971
- self.step.stderr = step_err.getvalue()
972
-
973
- # Clean up environment variables (to avoid potential confusion)
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
- pages = []
1008
- if not self.step.success:
1009
- # Format command such that it can be copied and pasted into a shell.
1010
- command = "stepup act "
1011
- if any(word.startswith("-") for word in shlex.split(self.step.action)):
1012
- command += "-- "
1013
- command += self.step.action
1014
- if self.step.workdir != "./":
1015
- command = f"(cd {self.step.workdir} && {command})"
1016
- lines = [f"Command {command}"]
1017
- # Other info on the execution of the step
1018
- if self.step.returncode is not None:
1019
- lines.append(f"Return code {self.step.returncode}")
1020
- pages.append(("Step info", "\n".join(lines)))
1021
- if len(self.step.perf_info) > 0:
1022
- pages.append(("Performance details", self.step.perf_info))
1023
- if self.step.rescheduled_info != "":
1024
- pages.append(
1025
- (
1026
- "Rescheduling due to unavailable amended inputs",
1027
- self.step.rescheduled_info,
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
- else:
1031
- if len(self.step.inp_messages) > 0:
1032
- self.step.inp_messages.sort()
1033
- pages.append(("Invalid inputs", "\n".join(self.step.inp_messages)))
1034
- if len(self.step.out_missing) > 0:
1035
- self.step.out_missing.sort()
1036
- pages.append(("Expected outputs not created", "\n".join(self.step.out_missing)))
1037
- stdout = self.step.stdout.rstrip()
1038
- if len(stdout) > 0:
1039
- pages.append(("Standard output", stdout))
1040
- stderr = self.step.stderr.rstrip()
1041
- if len(stderr) > 0:
1042
- pages.append(("Standard error", stderr))
1043
- if self.step.rescheduled_info != "":
1044
- action = "RESCHEDULE"
1045
- elif self.step.success:
1046
- action = "SUCCESS"
1047
- else:
1048
- action = "FAIL"
1049
- await self.reporter.stop_step(self.step.i)
1050
- await self.reporter(action, self.step.description, pages)
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
- # © 2024–2025 Toon Verstraelen
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stepup
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: StepUp Core provides the basic framework for the StepUp build tool
5
5
  Author-email: Toon Verstraelen <toon.verstraelen@ugent.be>
6
6
  License-Expression: GPL-3.0-or-later
@@ -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