stepup 3.2.1__tar.gz → 3.2.3__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.1/stepup.egg-info → stepup-3.2.3}/PKG-INFO +1 -1
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/browse.py +6 -4
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/cascade.py +3 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/clean.py +4 -3
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/director.py +26 -3
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/file.py +8 -4
- stepup-3.2.3/stepup/core/sqlite3.py +83 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/startup.py +1 -1
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/step.py +3 -3
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/tui.py +11 -1
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/utils.py +1 -17
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/worker.py +193 -153
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/workflow.py +8 -6
- {stepup-3.2.1 → stepup-3.2.3/stepup.egg-info}/PKG-INFO +1 -1
- {stepup-3.2.1 → stepup-3.2.3}/stepup.egg-info/SOURCES.txt +1 -0
- {stepup-3.2.1 → stepup-3.2.3}/LICENSE +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/MANIFEST.in +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/README.md +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/pyproject.toml +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/setup.cfg +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/__init__.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/__main__.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/actions.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/api.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/asyncio.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/call.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/deferred_glob.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/enums.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/exceptions.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/hash.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/interact.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/job.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/logo.svg +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/nglob.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/pytest.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/render_jinja.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/reporter.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/rpc.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/runner.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/scheduler.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/script.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/stepinfo.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup/core/watcher.py +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup.egg-info/dependency_links.txt +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup.egg-info/entry_points.txt +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup.egg-info/requires.txt +0 -0
- {stepup-3.2.1 → stepup-3.2.3}/stepup.egg-info/top_level.txt +0 -0
|
@@ -24,7 +24,6 @@ import contextlib
|
|
|
24
24
|
import importlib.resources
|
|
25
25
|
import os
|
|
26
26
|
import pickle
|
|
27
|
-
import sqlite3
|
|
28
27
|
import stat
|
|
29
28
|
import traceback
|
|
30
29
|
from collections.abc import Iterator
|
|
@@ -37,6 +36,7 @@ from path import Path
|
|
|
37
36
|
|
|
38
37
|
from .enums import FileState, Mandatory, StepState
|
|
39
38
|
from .hash import fmt_digest
|
|
39
|
+
from .sqlite3 import connect
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def browse_subcommand(subparser: argparse.ArgumentParser) -> callable:
|
|
@@ -275,8 +275,8 @@ class GraphServer(BaseHTTPRequestHandler):
|
|
|
275
275
|
print("Loading database...")
|
|
276
276
|
if self.con is not None:
|
|
277
277
|
self.con.close()
|
|
278
|
-
self.con =
|
|
279
|
-
src =
|
|
278
|
+
self.con = connect(":memory:")
|
|
279
|
+
src = connect(self.path_db)
|
|
280
280
|
try:
|
|
281
281
|
src.backup(self.con)
|
|
282
282
|
finally:
|
|
@@ -415,7 +415,9 @@ class GraphServer(BaseHTTPRequestHandler):
|
|
|
415
415
|
|
|
416
416
|
elif kind == "file":
|
|
417
417
|
(state_i, digest, mode, mtime, size, inode) = self.con.execute(
|
|
418
|
-
"SELECT state, digest, mode, mtime, size, inode
|
|
418
|
+
"SELECT state, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file "
|
|
419
|
+
"WHERE node = ?",
|
|
420
|
+
(node_i,),
|
|
419
421
|
).fetchone()
|
|
420
422
|
state = FileState(state_i)
|
|
421
423
|
yield f'<p><b>State:</b> <span class="{state.name.lower()}">{state.name}</span></p>'
|
|
@@ -562,6 +562,9 @@ class Cascade:
|
|
|
562
562
|
# While making this change, the enums were also made more intuitive.
|
|
563
563
|
# Schema 2 became outdated due to the worker actions.
|
|
564
564
|
# Schema 3 became outdated due to a change in step table (dirty field).
|
|
565
|
+
|
|
566
|
+
# Delayed Schema updates, for version 5:
|
|
567
|
+
# - Use UINT64 with PARSE_DECLTYPES instead of PARSE_COLNAMES.
|
|
565
568
|
return 4
|
|
566
569
|
|
|
567
570
|
@classmethod
|
|
@@ -29,7 +29,8 @@ from rich.console import Console
|
|
|
29
29
|
from .cascade import DROP_CONSUMERS, INITIAL_CONSUMERS, RECURSE_CONSUMERS
|
|
30
30
|
from .enums import FileState
|
|
31
31
|
from .hash import FileHash
|
|
32
|
-
from .
|
|
32
|
+
from .sqlite3 import copy_db_in_memory
|
|
33
|
+
from .utils import mynormpath, translate, translate_back
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def clean_subcommand(subparser: argparse.ArgumentParser) -> callable:
|
|
@@ -91,7 +92,7 @@ def clean_tool(args: argparse.Namespace):
|
|
|
91
92
|
# Copy the database in memory and work on the copy.
|
|
92
93
|
root = Path(os.getenv("STEPUP_ROOT", "."))
|
|
93
94
|
path_db = root / ".stepup/graph.db"
|
|
94
|
-
with
|
|
95
|
+
with copy_db_in_memory(path_db) as con:
|
|
95
96
|
clean(con, tr_paths, args)
|
|
96
97
|
|
|
97
98
|
|
|
@@ -175,7 +176,7 @@ def fmtnum(i: int):
|
|
|
175
176
|
CREATE_INITIAL_PATHS = "CREATE TABLE temp.initial_path(path TEXT PRIMARY KEY) WITHOUT ROWID"
|
|
176
177
|
|
|
177
178
|
SELECT_OUTPUTS = f"""
|
|
178
|
-
SELECT label, file.state, orphan, digest, mode, mtime, size, inode FROM node
|
|
179
|
+
SELECT label, file.state, orphan, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM node
|
|
179
180
|
JOIN all_consumer ON node.i = all_consumer.current
|
|
180
181
|
JOIN file ON file.node = all_consumer.current
|
|
181
182
|
WHERE file.state in
|
|
@@ -24,7 +24,6 @@ import asyncio
|
|
|
24
24
|
import logging
|
|
25
25
|
import os
|
|
26
26
|
import signal
|
|
27
|
-
import sqlite3
|
|
28
27
|
import sys
|
|
29
28
|
import time
|
|
30
29
|
import traceback
|
|
@@ -34,7 +33,10 @@ from importlib.metadata import version as get_version
|
|
|
34
33
|
import attrs
|
|
35
34
|
from path import Path
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
try:
|
|
37
|
+
import yappi
|
|
38
|
+
except ImportError:
|
|
39
|
+
yappi = None
|
|
38
40
|
|
|
39
41
|
from .asyncio import wait_for_events
|
|
40
42
|
from .enums import ReturnCode, StepState
|
|
@@ -45,7 +47,9 @@ from .reporter import ReporterClient
|
|
|
45
47
|
from .rpc import allow_rpc, serve_socket_rpc
|
|
46
48
|
from .runner import Runner
|
|
47
49
|
from .scheduler import Scheduler
|
|
50
|
+
from .sqlite3 import connect
|
|
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
|
|
@@ -71,6 +75,15 @@ async def async_main():
|
|
|
71
75
|
print(f"SOCKET {args.director_socket}", file=sys.stderr)
|
|
72
76
|
print(f"PID {os.getpid()}", file=sys.stderr)
|
|
73
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)
|
|
74
87
|
async with ReporterClient.socket(args.reporter_socket) as reporter:
|
|
75
88
|
num_workers = interpret_num_workers(args.num_workers)
|
|
76
89
|
await reporter.set_num_workers(num_workers)
|
|
@@ -95,6 +108,10 @@ async def async_main():
|
|
|
95
108
|
finally:
|
|
96
109
|
await reporter("DIRECTOR", "See you!")
|
|
97
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")
|
|
98
115
|
sys.exit(returncode.value)
|
|
99
116
|
|
|
100
117
|
|
|
@@ -170,6 +187,12 @@ def parse_args() -> argparse.Namespace:
|
|
|
170
187
|
action=argparse.BooleanOptionalAction,
|
|
171
188
|
help="Do not remove outdated output files.",
|
|
172
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
|
+
)
|
|
173
196
|
args = parser.parse_args()
|
|
174
197
|
if WATCHER_AVAILABLE:
|
|
175
198
|
if args.watch_first:
|
|
@@ -235,7 +258,7 @@ async def serve(
|
|
|
235
258
|
check_plan("plan.py")
|
|
236
259
|
|
|
237
260
|
# Create basic components
|
|
238
|
-
con =
|
|
261
|
+
con = connect(".stepup/graph.db")
|
|
239
262
|
dblock = DBLock(con)
|
|
240
263
|
workflow = Workflow(con)
|
|
241
264
|
scheduler = Scheduler(workflow.job_queue, workflow.config_queue, workflow.job_queue_changed)
|
|
@@ -29,6 +29,7 @@ from path import Path
|
|
|
29
29
|
from .cascade import Node
|
|
30
30
|
from .enums import DirWatch, FileState
|
|
31
31
|
from .hash import FileHash
|
|
32
|
+
from .sqlite3 import UInt64
|
|
32
33
|
from .utils import format_digest
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
@@ -102,7 +103,10 @@ class File(Node):
|
|
|
102
103
|
# If the file was previously BUILT or OUTDATED, and created again as AWAITED,
|
|
103
104
|
# it should copy that state
|
|
104
105
|
if state == FileState.AWAITED:
|
|
105
|
-
sql =
|
|
106
|
+
sql = (
|
|
107
|
+
"SELECT state, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file "
|
|
108
|
+
"WHERE node = ?"
|
|
109
|
+
)
|
|
106
110
|
row = self.con.execute(sql, (self.i,)).fetchone()
|
|
107
111
|
if row is not None and row[0] in (FileState.BUILT.value, FileState.OUTDATED.value):
|
|
108
112
|
state = FileState(row[0])
|
|
@@ -121,7 +125,7 @@ class File(Node):
|
|
|
121
125
|
"mode": mode,
|
|
122
126
|
"mtime": mtime,
|
|
123
127
|
"size": size,
|
|
124
|
-
"inode": inode,
|
|
128
|
+
"inode": UInt64(inode),
|
|
125
129
|
},
|
|
126
130
|
)
|
|
127
131
|
# If the state is BUILT, mark it as OUTDATED to force a rebuild.
|
|
@@ -215,7 +219,7 @@ class File(Node):
|
|
|
215
219
|
self.con.execute(sql, (state.value, self.i))
|
|
216
220
|
|
|
217
221
|
def get_hash(self) -> FileHash:
|
|
218
|
-
sql = "SELECT digest, mode, mtime, size, inode FROM file WHERE node = ?"
|
|
222
|
+
sql = "SELECT digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file WHERE node = ?"
|
|
219
223
|
row = self.con.execute(sql, (self.i,)).fetchone()
|
|
220
224
|
return FileHash(*row)
|
|
221
225
|
|
|
@@ -299,7 +303,7 @@ class File(Node):
|
|
|
299
303
|
# Local import to avoid cyclic imports.
|
|
300
304
|
from .step import Step # noqa: PLC0415
|
|
301
305
|
|
|
302
|
-
for step in self.consumers(Step):
|
|
306
|
+
for step in self.consumers(Step, include_orphans=True):
|
|
303
307
|
step.mark_pending()
|
|
304
308
|
elif state != FileState.OUTDATED:
|
|
305
309
|
raise ValueError(f"Cannot make file oudated when its state is {state}")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# StepUp Core provides the basic framework for the StepUp build tool.
|
|
2
|
+
# Copyright 2024-2026 Toon Verstraelen
|
|
3
|
+
#
|
|
4
|
+
# This file is part of StepUp Core.
|
|
5
|
+
#
|
|
6
|
+
# StepUp Core is free software; you can redistribute it and/or
|
|
7
|
+
# modify it under the terms of the GNU General Public License
|
|
8
|
+
# as published by the Free Software Foundation; either version 3
|
|
9
|
+
# of the License, or (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# StepUp Core is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU General Public License
|
|
17
|
+
# along with this program; if not, see <http://www.gnu.org/licenses/>
|
|
18
|
+
#
|
|
19
|
+
# --
|
|
20
|
+
"""Wrapper for SQLite3 functionality."""
|
|
21
|
+
|
|
22
|
+
import contextlib
|
|
23
|
+
import os
|
|
24
|
+
import sqlite3
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
from typing import Self
|
|
27
|
+
|
|
28
|
+
__all__ = ("UInt64", "connect", "copy_db_in_memory")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UInt64(int):
|
|
32
|
+
"""A wrapper to tell SQLite this int should be treated as an unsigned 64-bit value."""
|
|
33
|
+
|
|
34
|
+
MAX_SIGNED_64 = 2**63 - 1
|
|
35
|
+
MAX_WRAPAROUND_64 = 2**64
|
|
36
|
+
MAX_UNSIGNED_64 = MAX_WRAPAROUND_64 - 1
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def adapt(val: Self) -> int:
|
|
40
|
+
if not (0 <= val <= UInt64.MAX_UNSIGNED_64):
|
|
41
|
+
raise ValueError(f"Value {val} out of UINT64 range")
|
|
42
|
+
return val - UInt64.MAX_WRAPAROUND_64 if val > UInt64.MAX_SIGNED_64 else val
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def convert(val: bytes) -> Self:
|
|
46
|
+
val = int(val)
|
|
47
|
+
if val < 0:
|
|
48
|
+
val += UInt64.MAX_WRAPAROUND_64
|
|
49
|
+
return UInt64(val)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
sqlite3.register_adapter(UInt64, UInt64.adapt)
|
|
53
|
+
sqlite3.register_converter("UINT64", UInt64.convert)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def connect(path: str | os.PathLike[str], **kwargs) -> sqlite3.Connection:
|
|
57
|
+
"""Connect to a SQLite database, with the appropriate settings for StepUp.
|
|
58
|
+
|
|
59
|
+
The following deviations from the default settings are used:
|
|
60
|
+
|
|
61
|
+
- Types can be detected from column names,
|
|
62
|
+
which allows us to use the custom UINT64 type for file inodes.
|
|
63
|
+
- The `cached_statements` parameter is set to a large value to improve
|
|
64
|
+
performance when executing many similar statements.
|
|
65
|
+
"""
|
|
66
|
+
my_kwargs = {"cached_statements": 1024, "detect_types": sqlite3.PARSE_COLNAMES}
|
|
67
|
+
my_kwargs.update(kwargs)
|
|
68
|
+
return sqlite3.connect(path, **my_kwargs)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@contextlib.contextmanager
|
|
72
|
+
def copy_db_in_memory(path_db) -> Iterator[sqlite3.Connection]:
|
|
73
|
+
"""Copy an SQLite database into memory and yield the connection."""
|
|
74
|
+
dst = connect(":memory:")
|
|
75
|
+
try:
|
|
76
|
+
src = connect(path_db)
|
|
77
|
+
try:
|
|
78
|
+
src.backup(dst)
|
|
79
|
+
finally:
|
|
80
|
+
src.close()
|
|
81
|
+
yield dst
|
|
82
|
+
finally:
|
|
83
|
+
dst.close()
|
|
@@ -124,7 +124,7 @@ async def scan_file_changes(
|
|
|
124
124
|
) -> tuple[set[str], set[str]]:
|
|
125
125
|
"""Check all files in the workflow for changes."""
|
|
126
126
|
sql = (
|
|
127
|
-
"SELECT label, state, digest, mode, mtime, size, inode "
|
|
127
|
+
"SELECT label, state, digest, mode, mtime, size, inode AS 'inode [UINT64]' "
|
|
128
128
|
"FROM node JOIN file ON node.i = file.node AND state NOT IN (?, ?) AND NOT orphan"
|
|
129
129
|
)
|
|
130
130
|
data = (FileState.AWAITED.value, FileState.VOLATILE.value)
|
|
@@ -558,7 +558,7 @@ class Step(Node):
|
|
|
558
558
|
fields.append("state")
|
|
559
559
|
join_file = True
|
|
560
560
|
if yield_hash:
|
|
561
|
-
fields.extend(["digest", "mode", "mtime", "size", "inode"])
|
|
561
|
+
fields.extend(["digest", "mode", "mtime", "size", "inode AS 'inode [UINT64]'"])
|
|
562
562
|
join_file = True
|
|
563
563
|
if yield_orphan:
|
|
564
564
|
fields.append("orphan")
|
|
@@ -731,7 +731,7 @@ class Step(Node):
|
|
|
731
731
|
sql = (
|
|
732
732
|
"SELECT node.label, node.orphan, file.state, "
|
|
733
733
|
"EXISTS (SELECT 1 FROM amended_dep WHERE amended_dep.i = dep.i), "
|
|
734
|
-
"file.digest, file.mode, file.mtime, file.size, file.inode "
|
|
734
|
+
"file.digest, file.mode, file.mtime, file.size, file.inode AS 'inode [UINT64]' "
|
|
735
735
|
"FROM node JOIN dependency AS dep ON node.i = dep.supplier "
|
|
736
736
|
"JOIN file ON file.node = node.i "
|
|
737
737
|
"WHERE dep.consumer = ?"
|
|
@@ -987,6 +987,6 @@ class Step(Node):
|
|
|
987
987
|
logger.info("Mark %s step PENDING: %s", state.name, self.label)
|
|
988
988
|
self.set_state(StepState.PENDING)
|
|
989
989
|
# First make all consumers (output files) pending
|
|
990
|
-
for file in self.consumers(File):
|
|
990
|
+
for file in self.consumers(File, include_orphans=True):
|
|
991
991
|
if file.get_state() == FileState.BUILT:
|
|
992
992
|
file.mark_outdated()
|
|
@@ -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
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"""Small utilities used throughout."""
|
|
21
21
|
|
|
22
22
|
import asyncio
|
|
23
|
-
import contextlib
|
|
24
23
|
import logging
|
|
25
24
|
import os
|
|
26
25
|
import re
|
|
@@ -28,7 +27,7 @@ import shlex
|
|
|
28
27
|
import sqlite3
|
|
29
28
|
import string
|
|
30
29
|
import sys
|
|
31
|
-
from collections.abc import Collection
|
|
30
|
+
from collections.abc import Collection
|
|
32
31
|
|
|
33
32
|
import attrs
|
|
34
33
|
from path import Path
|
|
@@ -429,18 +428,3 @@ def string_to_bool(v: str | bool) -> bool:
|
|
|
429
428
|
return False
|
|
430
429
|
raise ValueError(f"Cannot interpret '{v}' as a boolean value.")
|
|
431
430
|
raise TypeError(f"Expected a boolean value or string. Got {type(v).__name__}")
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
@contextlib.contextmanager
|
|
435
|
-
def sqlite3_copy_in_memory(path_db) -> Iterator[sqlite3.Connection]:
|
|
436
|
-
"""Copy an SQLite database into memory and yield the connection."""
|
|
437
|
-
dst = sqlite3.Connection(":memory:")
|
|
438
|
-
try:
|
|
439
|
-
src = sqlite3.Connection(path_db)
|
|
440
|
-
try:
|
|
441
|
-
src.backup(dst)
|
|
442
|
-
finally:
|
|
443
|
-
src.close()
|
|
444
|
-
yield dst
|
|
445
|
-
finally:
|
|
446
|
-
dst.close()
|
|
@@ -25,6 +25,7 @@ import contextlib
|
|
|
25
25
|
import ctypes
|
|
26
26
|
import inspect
|
|
27
27
|
import io
|
|
28
|
+
import json
|
|
28
29
|
import logging
|
|
29
30
|
import os
|
|
30
31
|
import queue
|
|
@@ -228,8 +229,6 @@ class WorkerClient:
|
|
|
228
229
|
return True
|
|
229
230
|
|
|
230
231
|
# Delegate the calculation of the output part of the step hash to the worker.
|
|
231
|
-
# With skipping=True, the worker knows the outputs should not have changed
|
|
232
|
-
# and will report it on screen.
|
|
233
232
|
new_step_hash, new_out_hashes = await self.compute_out_step_hash(step, new_step_hash)
|
|
234
233
|
|
|
235
234
|
if step_hash.out_digest != new_step_hash.out_digest:
|
|
@@ -707,6 +706,38 @@ def check_executable(executable: Path, shebang: str | None = None) -> bool:
|
|
|
707
706
|
return True
|
|
708
707
|
|
|
709
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
|
+
|
|
710
741
|
@attrs.define
|
|
711
742
|
class WorkerHandler:
|
|
712
743
|
"""RPC Handler in the worker process to respond to requests from the WorkerClient."""
|
|
@@ -716,6 +747,7 @@ class WorkerHandler:
|
|
|
716
747
|
explain_rerun: bool = attrs.field()
|
|
717
748
|
stop_event: asyncio.Event = attrs.field(factory=asyncio.Event)
|
|
718
749
|
step: WorkerStep | None = attrs.field(init=False, default=None)
|
|
750
|
+
timer: Timer = attrs.field(init=False, factory=Timer)
|
|
719
751
|
|
|
720
752
|
@allow_rpc
|
|
721
753
|
def shutdown(self):
|
|
@@ -777,18 +809,19 @@ class WorkerHandler:
|
|
|
777
809
|
The new hash of the step, with the input part already computed, if available.
|
|
778
810
|
`None` is yielded if, unexpectedly, some inputs are missing or have changed.
|
|
779
811
|
"""
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
+
)
|
|
785
818
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
819
|
+
# Create the step
|
|
820
|
+
action, workdir = split_step_label(label)
|
|
821
|
+
self.step = WorkerStep(i, action, workdir)
|
|
789
822
|
|
|
790
|
-
|
|
791
|
-
|
|
823
|
+
# Create initial StepHash
|
|
824
|
+
return self.compute_inp_step_hash(inp_hashes, env_vars, check_hash)[0]
|
|
792
825
|
|
|
793
826
|
def compute_inp_step_hash(
|
|
794
827
|
self,
|
|
@@ -819,41 +852,44 @@ class WorkerHandler:
|
|
|
819
852
|
so they can be updated in StepUp's workflow database if desired.
|
|
820
853
|
Unchanged ones are not included.
|
|
821
854
|
"""
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if new_file_hash
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
|
844
878
|
|
|
845
|
-
|
|
846
|
-
|
|
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]
|
|
847
881
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
+
)
|
|
853
889
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
890
|
+
# Copy the inp_digest, because it can be useful for some actions.
|
|
891
|
+
self.step.inp_digest = result.inp_digest
|
|
892
|
+
return result, []
|
|
857
893
|
|
|
858
894
|
@allow_rpc
|
|
859
895
|
def compute_out_step_hash(
|
|
@@ -881,22 +917,23 @@ class WorkerHandler:
|
|
|
881
917
|
so they can be updated in StepUp's workflow database if desired.
|
|
882
918
|
Unchanged ones are not included.
|
|
883
919
|
"""
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
|
895
932
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
|
900
937
|
|
|
901
938
|
@allow_rpc
|
|
902
939
|
def compute_full_step_hash(
|
|
@@ -956,57 +993,58 @@ class WorkerHandler:
|
|
|
956
993
|
|
|
957
994
|
@allow_rpc
|
|
958
995
|
async def run(self):
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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.
|
|
978
1035
|
if self.show_perf:
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
del os.environ["STEPUP_STEP_I"]
|
|
992
|
-
del os.environ["STEPUP_STEP_INP_DIGEST"]
|
|
993
|
-
del os.environ["ROOT"]
|
|
994
|
-
del os.environ["HERE"]
|
|
995
|
-
|
|
996
|
-
# Process results of the step.
|
|
997
|
-
if self.show_perf:
|
|
998
|
-
ru_final = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
999
|
-
utime = ru_final.ru_utime - ru_initial.ru_utime
|
|
1000
|
-
stime = ru_final.ru_stime - ru_initial.ru_stime
|
|
1001
|
-
wtime = perf_counter() - pt_initial
|
|
1002
|
-
ru_lines = [
|
|
1003
|
-
f"User CPU time [s]: {utime:9.4f}",
|
|
1004
|
-
f"System CPU time [s]: {stime:9.4f}",
|
|
1005
|
-
f"Total CPU time [s]: {utime + stime:9.4f}",
|
|
1006
|
-
f"Wall time [s]: {wtime:9.4f}",
|
|
1007
|
-
]
|
|
1008
|
-
self.step.perf_info = "\n".join(ru_lines)
|
|
1009
|
-
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
|
|
1010
1048
|
|
|
1011
1049
|
@allow_rpc
|
|
1012
1050
|
def get_success(self) -> bool:
|
|
@@ -1021,51 +1059,52 @@ class WorkerHandler:
|
|
|
1021
1059
|
|
|
1022
1060
|
@allow_rpc
|
|
1023
1061
|
async def report(self):
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
(
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
+
)
|
|
1045
1085
|
)
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
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
|
|
1069
1108
|
|
|
1070
1109
|
@allow_rpc
|
|
1071
1110
|
async def skip(self, step_hash: StepHash):
|
|
@@ -1155,6 +1194,7 @@ async def async_main():
|
|
|
1155
1194
|
# Create the worker handler for the RPC server.
|
|
1156
1195
|
handler = WorkerHandler(reporter, args.show_perf, args.explain_rerun)
|
|
1157
1196
|
await serve_socket_rpc(handler, args.worker_socket, handler.stop_event)
|
|
1197
|
+
handler.timer.report()
|
|
1158
1198
|
|
|
1159
1199
|
|
|
1160
1200
|
def main():
|
|
@@ -39,6 +39,7 @@ from .exceptions import GraphError
|
|
|
39
39
|
from .file import File
|
|
40
40
|
from .hash import FileHash, fmt_digest
|
|
41
41
|
from .nglob import NGlobMulti, convert_nglob_to_regex, iter_wildcard_names
|
|
42
|
+
from .sqlite3 import UInt64
|
|
42
43
|
from .step import Step
|
|
43
44
|
from .utils import myparent, string_to_bool
|
|
44
45
|
|
|
@@ -568,13 +569,14 @@ class Workflow(Cascade):
|
|
|
568
569
|
sql = "INSERT INTO temp.missing VALUES (?)"
|
|
569
570
|
self.con.executemany(sql, ((file.i,) for file in deferred))
|
|
570
571
|
sql = (
|
|
571
|
-
"SELECT label, digest,
|
|
572
|
+
"SELECT label, digest, mode, mtime, size, inode AS 'inode [UINT64]' "
|
|
573
|
+
"FROM temp.missing "
|
|
572
574
|
"JOIN node ON node.i = temp.missing.node "
|
|
573
575
|
"JOIN file ON file.node = temp.missing.node"
|
|
574
576
|
)
|
|
575
577
|
return [
|
|
576
|
-
(path, FileHash(digest,
|
|
577
|
-
for path, digest,
|
|
578
|
+
(path, FileHash(digest, mode, mtime, size, inode))
|
|
579
|
+
for path, digest, mode, mtime, size, inode in self.con.execute(sql)
|
|
578
580
|
]
|
|
579
581
|
finally:
|
|
580
582
|
self.con.execute("DROP TABLE IF EXISTS temp.missing")
|
|
@@ -597,7 +599,7 @@ class Workflow(Cascade):
|
|
|
597
599
|
self.con.execute("CREATE TABLE temp.paths(path TEXT PRIMARY KEY)")
|
|
598
600
|
self.con.executemany("INSERT INTO temp.paths VALUES (?)", ((path,) for path in paths))
|
|
599
601
|
sql = (
|
|
600
|
-
"SELECT label, digest, mode, mtime, size, inode FROM node "
|
|
602
|
+
"SELECT label, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM node "
|
|
601
603
|
"JOIN file ON file.node = node.i JOIN temp.paths ON label = temp.paths.path"
|
|
602
604
|
)
|
|
603
605
|
return [
|
|
@@ -735,7 +737,7 @@ class Workflow(Cascade):
|
|
|
735
737
|
"UPDATE file SET state = ?, digest = ?, mode = ?, mtime = ?, size = ?, inode = ? "
|
|
736
738
|
"WHERE node = ?",
|
|
737
739
|
(
|
|
738
|
-
(state.value, fh.digest, fh.mode, fh.mtime, fh.size, fh.inode, i)
|
|
740
|
+
(state.value, fh.digest, fh.mode, fh.mtime, fh.size, UInt64(fh.inode), i)
|
|
739
741
|
for i, state, fh in new_states_hashes
|
|
740
742
|
),
|
|
741
743
|
)
|
|
@@ -1141,7 +1143,7 @@ class Workflow(Cascade):
|
|
|
1141
1143
|
file.orphan()
|
|
1142
1144
|
# Delete outputs of steps that are no longer mandatory.
|
|
1143
1145
|
cur = self.con.execute(
|
|
1144
|
-
"SELECT label, digest, mode, mtime, size, inode FROM file "
|
|
1146
|
+
"SELECT label, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file "
|
|
1145
1147
|
"JOIN node ON node.i = file.node "
|
|
1146
1148
|
"JOIN dependency ON node.i = consumer "
|
|
1147
1149
|
"JOIN step ON step.node = supplier "
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|