fwm 0.1.1__py3-none-any.whl
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.
- fwm/__init__.py +0 -0
- fwm/__main__.py +3 -0
- fwm/cli/__init__.py +0 -0
- fwm/cli/completion.py +191 -0
- fwm/cli/fwm.py +174 -0
- fwm/cli/output.py +72 -0
- fwm/cli/subcmds/__init__.py +0 -0
- fwm/cli/subcmds/bootstrap.py +166 -0
- fwm/cli/subcmds/completion.py +32 -0
- fwm/cli/subcmds/compose.py +280 -0
- fwm/cli/subcmds/list.py +96 -0
- fwm/cli/subcmds/mkgen.py +141 -0
- fwm/cli/subcmds/repo.py +263 -0
- fwm/cli/subcmds/show.py +56 -0
- fwm/cli/subcmds/spawn.py +39 -0
- fwm/data_loader/__init__.py +0 -0
- fwm/data_loader/checkpoint.py +192 -0
- fwm/data_loader/ckpt_desc.py +69 -0
- fwm/data_loader/entry_point.py +52 -0
- fwm/data_loader/file_catalog.py +76 -0
- fwm/data_loader/final_state.py +114 -0
- fwm/data_loader/provenance.py +125 -0
- fwm/data_loader/recording.py +266 -0
- fwm/data_loader/sim_version.py +67 -0
- fwm/data_loader/simpoint_info.py +43 -0
- fwm/database/__init__.py +0 -0
- fwm/database/catalog_store.py +69 -0
- fwm/database/custom_field.py +24 -0
- fwm/database/db.py +80 -0
- fwm/database/schema.py +335 -0
- fwm/mkgen/__init__.py +0 -0
- fwm/mkgen/makefile.py +304 -0
- fwm/mkgen/run_json.py +45 -0
- fwm/mkgen/templates/create-checkpoints.mk +15 -0
- fwm/mkgen/templates/from-checkpoint.mk +16 -0
- fwm/mkgen/templates/from-fesvr-trace.mk +13 -0
- fwm/mkgen/templates/from-simpoint.mk +18 -0
- fwm/mkgen/templates/from-sysroot.mk +28 -0
- fwm/mkgen/templates/header.mk +27 -0
- fwm/mkgen/templates/record.mk +28 -0
- fwm/py.typed +0 -0
- fwm/repo/__init__.py +0 -0
- fwm/repo/checkpoint.py +220 -0
- fwm/repo/entry.py +139 -0
- fwm/repo/errors.py +24 -0
- fwm/repo/gc.py +134 -0
- fwm/repo/listing.py +133 -0
- fwm/repo/lookup.py +102 -0
- fwm/repo/query.py +136 -0
- fwm/repo/remove.py +81 -0
- fwm/repo/repo.py +218 -0
- fwm/repo/spawn.py +118 -0
- fwm/repo/subrepo.py +63 -0
- fwm/repo/sysroot.py +80 -0
- fwm/repo/trace.py +218 -0
- fwm/repo/transfer.py +567 -0
- fwm/repo/validation.py +73 -0
- fwm/utils/__init__.py +0 -0
- fwm/utils/digest.py +44 -0
- fwm/utils/file_catalog.py +213 -0
- fwm/utils/fs.py +149 -0
- fwm/utils/log.py +77 -0
- fwm/utils/target_path.py +38 -0
- fwm/version.py +46 -0
- fwm-0.1.1.dist-info/METADATA +353 -0
- fwm-0.1.1.dist-info/RECORD +69 -0
- fwm-0.1.1.dist-info/WHEEL +4 -0
- fwm-0.1.1.dist-info/entry_points.txt +3 -0
- fwm-0.1.1.dist-info/licenses/LICENSE +33 -0
fwm/__init__.py
ADDED
|
File without changes
|
fwm/__main__.py
ADDED
fwm/cli/__init__.py
ADDED
|
File without changes
|
fwm/cli/completion.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell completion of the names of repository content: sysroots, workloads, entry points, traces and checkpoints.
|
|
3
|
+
|
|
4
|
+
Click calls these while it parses the command line for completion, without running the command callbacks, so the
|
|
5
|
+
repository is opened here from the parsed --repo-path (or FWM_REPO_PATH). Completion must never fail loudly: without a
|
|
6
|
+
usable repository, or on any error, a callback offers nothing.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
from typing import Callable, Dict, Iterable, Iterator, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from click.shell_completion import CompletionItem
|
|
13
|
+
|
|
14
|
+
from fwm.database.schema import Sysroot, Workload, NativeEntry, DecoupledTrace, DecoupledCheckpoint
|
|
15
|
+
from fwm.repo.listing import natural_order
|
|
16
|
+
from fwm.repo.repo import Repo
|
|
17
|
+
from fwm.repo.lookup import resolve_trace
|
|
18
|
+
|
|
19
|
+
Candidates = Iterable[Tuple[str, Optional[str]]] # (value, help)
|
|
20
|
+
Completer = Callable[[click.Context, click.Parameter, str], List[CompletionItem]]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _expand(path: Optional[str]) -> Optional[str]:
|
|
24
|
+
# the shell expands a leading ~ when it runs a command, not in the words it passes to complete one
|
|
25
|
+
return os.path.expanduser(path) if path else path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _repo_path(ctx: click.Context) -> Optional[str]:
|
|
29
|
+
"""The repository of --repo-path (or FWM_REPO_PATH)."""
|
|
30
|
+
return _expand(ctx.find_root().params.get("repo_path"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _source_path(ctx: click.Context) -> Optional[str]:
|
|
34
|
+
"""The repository of the SOURCE argument (fwm repo pull)."""
|
|
35
|
+
return _expand(ctx.params.get("source"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _open_repo_quietly(path: Optional[str]) -> Optional[Repo]:
|
|
39
|
+
if not path:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
return Repo.open(path, readonly=True)
|
|
43
|
+
except Exception:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _completer(candidates: Callable[[click.Context, str], Candidates],
|
|
48
|
+
repo_path: Callable[[click.Context], Optional[str]] = _repo_path) -> Completer:
|
|
49
|
+
"""
|
|
50
|
+
Make a click shell_complete callback that offers the candidates starting with the incomplete value, naturally
|
|
51
|
+
sorted, from the repository given on the command line: by default --repo-path.
|
|
52
|
+
"""
|
|
53
|
+
def complete(ctx: click.Context, param: click.Parameter, incomplete: str) -> List[CompletionItem]:
|
|
54
|
+
repo = _open_repo_quietly(repo_path(ctx))
|
|
55
|
+
if repo is None:
|
|
56
|
+
return []
|
|
57
|
+
try:
|
|
58
|
+
items = {value: help_text for value, help_text in candidates(ctx, incomplete)
|
|
59
|
+
if value.startswith(incomplete)}
|
|
60
|
+
return [CompletionItem(value, help=items[value]) for value in natural_order(items)]
|
|
61
|
+
except Exception:
|
|
62
|
+
return []
|
|
63
|
+
finally:
|
|
64
|
+
repo.close()
|
|
65
|
+
|
|
66
|
+
return complete
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _count(n: int, what: str) -> str:
|
|
70
|
+
return f"{n} {what}{'' if n == 1 else 's'}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _sysroots(ctx: click.Context, incomplete: str) -> Candidates:
|
|
74
|
+
for sysroot in Sysroot.select():
|
|
75
|
+
yield sysroot.name, _count(sysroot.entries.count(), "entry point")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _workloads(ctx: click.Context, incomplete: str) -> Candidates:
|
|
79
|
+
for workload in Workload.select():
|
|
80
|
+
yield workload.name, _count(workload.entries.count(), "entry point")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _entries(ctx: click.Context, incomplete: str) -> Candidates:
|
|
84
|
+
for entry in NativeEntry.select():
|
|
85
|
+
yield entry.name, f"{entry.workload.name} @ {entry.sysroot.name}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _other_entries(ctx: click.Context, incomplete: str) -> Candidates:
|
|
89
|
+
"""The entry points not given yet to the ENTRIES argument (fwm repo pull, push and subrepo), unless --all is."""
|
|
90
|
+
if ctx.params.get("everything"):
|
|
91
|
+
return []
|
|
92
|
+
given = set(ctx.params.get("entries") or ())
|
|
93
|
+
return ((name, help_text) for name, help_text in _entries(ctx, incomplete) if name not in given)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _trace_help(trace: DecoupledTrace) -> str:
|
|
97
|
+
state = "complete" if trace.exit_status is not None else "truncated"
|
|
98
|
+
return f"{trace.native_entry.name}, {state}, {_count(trace.checkpoints.count(), 'checkpoint')}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _traces(ctx: click.Context, incomplete: str) -> Candidates:
|
|
102
|
+
"""Trace UUIDs, and the entry points with exactly one trace, which name it too."""
|
|
103
|
+
traces_of: Dict[str, List[DecoupledTrace]] = {}
|
|
104
|
+
for trace in DecoupledTrace.select():
|
|
105
|
+
traces_of.setdefault(trace.native_entry.name, []).append(trace)
|
|
106
|
+
yield str(trace.uuid), _trace_help(trace)
|
|
107
|
+
for entry_name, traces in traces_of.items():
|
|
108
|
+
if len(traces) == 1:
|
|
109
|
+
yield entry_name, f"its trace {traces[0].uuid}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _checkpoint_help(ckpt: DecoupledCheckpoint) -> str:
|
|
113
|
+
weight = f", weight {ckpt.simpoint_weight}" if ckpt.simpoint_weight is not None else ""
|
|
114
|
+
return f"{ckpt.trace.native_entry.name}{weight}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _checkpoint_refs(incomplete: str) -> Iterator[Tuple[str, DecoupledCheckpoint]]:
|
|
118
|
+
"""
|
|
119
|
+
Checkpoints as resolve_checkpoint() accepts them, each once: by its name when the name is unique in the repository,
|
|
120
|
+
otherwise as <trace UUID>:<name>.
|
|
121
|
+
|
|
122
|
+
After a typed ':', offer the checkpoints of the trace before it (a UUID prefix or an entry point with one trace),
|
|
123
|
+
keeping that prefix as typed.
|
|
124
|
+
"""
|
|
125
|
+
if ":" in incomplete:
|
|
126
|
+
trace_ref = incomplete.split(":", 1)[0]
|
|
127
|
+
trace = resolve_trace(trace_ref)
|
|
128
|
+
for ckpt in trace.checkpoints:
|
|
129
|
+
yield f"{trace_ref}:{ckpt.name}", ckpt
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
by_name: Dict[str, List[DecoupledCheckpoint]] = {}
|
|
133
|
+
for ckpt in DecoupledCheckpoint.select():
|
|
134
|
+
by_name.setdefault(ckpt.name, []).append(ckpt)
|
|
135
|
+
for name, ckpts in by_name.items():
|
|
136
|
+
if len(ckpts) == 1:
|
|
137
|
+
yield name, ckpts[0]
|
|
138
|
+
else:
|
|
139
|
+
for ckpt in ckpts:
|
|
140
|
+
yield f"{ckpt.trace.uuid}:{name}", ckpt
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _checkpoints(ctx: click.Context, incomplete: str) -> Candidates:
|
|
144
|
+
return ((ref, _checkpoint_help(ckpt)) for ref, ckpt in _checkpoint_refs(incomplete))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _simpoint_checkpoints(ctx: click.Context, incomplete: str) -> Candidates:
|
|
148
|
+
"""The checkpoints that have SimPoint information (fwm mkgen from-simpoint)."""
|
|
149
|
+
return ((ref, _checkpoint_help(ckpt)) for ref, ckpt in _checkpoint_refs(incomplete)
|
|
150
|
+
if ckpt.simpoint_interval is not None)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_BY_KIND: Dict[str, Callable[[click.Context, str], Candidates]] = {
|
|
154
|
+
"workload": _workloads,
|
|
155
|
+
"sysroot": _sysroots,
|
|
156
|
+
"entry": _entries,
|
|
157
|
+
"trace": _traces,
|
|
158
|
+
"checkpoint": _checkpoints,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _by_kind(ctx: click.Context, incomplete: str) -> Candidates:
|
|
163
|
+
"""The ID of the kind given by the preceding KIND argument (show, repo compose remove, repo verify)."""
|
|
164
|
+
candidates = _BY_KIND.get(ctx.params.get("kind") or "")
|
|
165
|
+
return candidates(ctx, incomplete) if candidates else []
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _checkpoint_names(ctx: click.Context, incomplete: str) -> Candidates:
|
|
169
|
+
"""Checkpoint names, each once (`fwm list --checkpoint` matches names, or <full trace UUID glob>:<name glob>)."""
|
|
170
|
+
counts: Dict[str, int] = {}
|
|
171
|
+
for ckpt in DecoupledCheckpoint.select(DecoupledCheckpoint.name):
|
|
172
|
+
counts[ckpt.name] = counts.get(ckpt.name, 0) + 1
|
|
173
|
+
return ((name, _count(n, "checkpoint")) for name, n in counts.items())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _trace_uuids(ctx: click.Context, incomplete: str) -> Candidates:
|
|
177
|
+
"""Trace UUIDs only (`fwm list --trace` matches UUIDs, not the names of entry points)."""
|
|
178
|
+
return ((str(trace.uuid), _trace_help(trace)) for trace in DecoupledTrace.select())
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
complete_sysroot = _completer(_sysroots)
|
|
182
|
+
complete_workload = _completer(_workloads)
|
|
183
|
+
complete_entry = _completer(_entries)
|
|
184
|
+
complete_other_entry = _completer(_other_entries)
|
|
185
|
+
complete_source_entry = _completer(_other_entries, _source_path)
|
|
186
|
+
complete_trace = _completer(_traces)
|
|
187
|
+
complete_trace_uuid = _completer(_trace_uuids)
|
|
188
|
+
complete_checkpoint_name = _completer(_checkpoint_names)
|
|
189
|
+
complete_checkpoint = _completer(_checkpoints)
|
|
190
|
+
complete_simpoint_checkpoint = _completer(_simpoint_checkpoints)
|
|
191
|
+
complete_by_kind = _completer(_by_kind)
|
fwm/cli/fwm.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import importlib
|
|
3
|
+
import signal
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Sequence, Tuple
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from fwm.repo.errors import FwmError
|
|
9
|
+
from fwm.repo.repo import Repo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _exit_on_signal(signum, frame):
|
|
13
|
+
"""End like Ctrl-C does: by an exception, so the cleanup of an interrupted import or removal runs."""
|
|
14
|
+
raise SystemExit(128 + signum)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# The module, attribute and short help of each subcommand. A subcommand is imported only when it runs, since loading
|
|
18
|
+
# every module from a shared filesystem takes seconds. Its short help is kept here, so that `fwm --help` and completing
|
|
19
|
+
# a subcommand name import none of them
|
|
20
|
+
_SUBCOMMANDS = {
|
|
21
|
+
"repo": ("fwm.cli.subcmds.repo", "repo", "Create and maintain a repository: its creator's commands."),
|
|
22
|
+
"list": ("fwm.cli.subcmds.list", "list_cmd", "List workloads, sysroots, entries, traces or checkpoints."),
|
|
23
|
+
"show": ("fwm.cli.subcmds.show", "show", "Show one workload, sysroot, entry, trace or checkpoint."),
|
|
24
|
+
"spawn": ("fwm.cli.subcmds.spawn", "spawn", "Make a working copy of an entry point's sysroot."),
|
|
25
|
+
"mkgen": ("fwm.cli.subcmds.mkgen", "mkgen", "Generate a run directory that starts a simulation."),
|
|
26
|
+
"completion": ("fwm.cli.subcmds.completion", "completion", "Print the shell completion script."),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# The sections of `fwm --help`: the daily commands of a repository's users first
|
|
31
|
+
_SECTIONS = (
|
|
32
|
+
("Daily", ("list", "show", "mkgen", "spawn")),
|
|
33
|
+
("Setup and maintenance", ("completion", "repo")),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Not offered when completing a subcommand name: once completion works, its script is already installed
|
|
37
|
+
_NOT_COMPLETED = ("completion",)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_sections(formatter: click.HelpFormatter, sections: Iterable[Tuple[str, Sequence[str]]],
|
|
41
|
+
short_help: Dict[str, str]) -> None:
|
|
42
|
+
"""Write the commands of short_help (name: short help) under titled sections; the others last, under "Commands"."""
|
|
43
|
+
in_sections = {name for _, names in sections for name in names}
|
|
44
|
+
rest = sorted(name for name in short_help if name not in in_sections)
|
|
45
|
+
width = max(len(name) for name in short_help) # one column of names across the sections
|
|
46
|
+
for title, names in [*sections, ("Commands", rest)]:
|
|
47
|
+
rows = [(name.ljust(width), short_help[name]) for name in names if name in short_help]
|
|
48
|
+
if rows:
|
|
49
|
+
with formatter.section(title):
|
|
50
|
+
formatter.write_dl(rows)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SectionedGroup(click.Group):
|
|
54
|
+
"""A command group whose help lists its commands under titled sections: sections=[(title, [name, ...]), ...]."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, *args: Any, sections: Sequence[Tuple[str, Sequence[str]]] = (), **kwargs: Any):
|
|
57
|
+
super().__init__(*args, **kwargs)
|
|
58
|
+
self.sections = sections
|
|
59
|
+
|
|
60
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
61
|
+
commands = {name: self.get_command(ctx, name) for name in self.list_commands(ctx)}
|
|
62
|
+
visible = {name: command for name, command in commands.items() if command is not None and not command.hidden}
|
|
63
|
+
if not visible:
|
|
64
|
+
return
|
|
65
|
+
limit = formatter.width - 6 - max(len(name) for name in visible)
|
|
66
|
+
format_sections(formatter, self.sections,
|
|
67
|
+
{name: command.get_short_help_str(limit) for name, command in visible.items()})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FwmGroup(click.Group):
|
|
71
|
+
"""
|
|
72
|
+
The fwm command group: imports a subcommand when it runs, reports an FwmError as a message, and exits with its
|
|
73
|
+
exit code.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def list_commands(self, ctx: click.Context) -> List[str]:
|
|
77
|
+
return sorted(_SUBCOMMANDS)
|
|
78
|
+
|
|
79
|
+
def get_command(self, ctx: click.Context, cmd_name: str):
|
|
80
|
+
if cmd_name not in self.commands and cmd_name in _SUBCOMMANDS:
|
|
81
|
+
module, attr, short_help = _SUBCOMMANDS[cmd_name]
|
|
82
|
+
command = getattr(importlib.import_module(module), attr)
|
|
83
|
+
command.short_help = short_help
|
|
84
|
+
self.add_command(command, cmd_name)
|
|
85
|
+
return super().get_command(ctx, cmd_name)
|
|
86
|
+
|
|
87
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
88
|
+
format_sections(formatter, _SECTIONS, {name: _SUBCOMMANDS[name][2] for name in self.list_commands(ctx)})
|
|
89
|
+
|
|
90
|
+
def shell_complete(self, ctx: click.Context, incomplete: str):
|
|
91
|
+
from click.shell_completion import CompletionItem
|
|
92
|
+
|
|
93
|
+
names = [CompletionItem(name, help=_SUBCOMMANDS[name][2])
|
|
94
|
+
for name in self.list_commands(ctx) if name.startswith(incomplete) and name not in _NOT_COMPLETED]
|
|
95
|
+
# the options: click.Group's own version would also import every subcommand for its help
|
|
96
|
+
return names + click.Command.shell_complete(self, ctx, incomplete)
|
|
97
|
+
|
|
98
|
+
def main(self, *args, **kwargs):
|
|
99
|
+
# Batch schedulers stop a job with SIGTERM (or SIGHUP), whose default action skips all cleanup
|
|
100
|
+
for sig in (signal.SIGTERM, signal.SIGHUP):
|
|
101
|
+
signal.signal(sig, _exit_on_signal)
|
|
102
|
+
return super().main(*args, **kwargs)
|
|
103
|
+
|
|
104
|
+
def invoke(self, ctx: click.Context):
|
|
105
|
+
try:
|
|
106
|
+
return super().invoke(ctx)
|
|
107
|
+
except FwmError as e:
|
|
108
|
+
click.echo(f"error: {e}", err=True)
|
|
109
|
+
ctx.exit(e.exit_code)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None:
|
|
113
|
+
if not value or ctx.resilient_parsing:
|
|
114
|
+
return
|
|
115
|
+
# imported here: finding the version loads many modules, which the other commands don't need
|
|
116
|
+
from fwm.version import fwm_build
|
|
117
|
+
click.echo(f"fwm {fwm_build()}")
|
|
118
|
+
ctx.exit()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@click.group(cls=FwmGroup)
|
|
122
|
+
@click.pass_context
|
|
123
|
+
@click.option("--version", is_flag=True, expose_value=False, is_eager=True, callback=_print_version,
|
|
124
|
+
help="Print which fwm this is, and exit.")
|
|
125
|
+
@click.option("--repo-path", envvar='FWM_REPO_PATH', type=click.Path(file_okay=False),
|
|
126
|
+
help="The workload repository (default: the environment variable FWM_REPO_PATH).")
|
|
127
|
+
def cli(ctx, repo_path):
|
|
128
|
+
"""
|
|
129
|
+
The RISC-V FESVR Workload Manager: find the workloads of a repository, and generate the runs that simulate them.
|
|
130
|
+
|
|
131
|
+
Search the repository with `fwm list` and its filters, and see the details of what you find with `fwm show`. The
|
|
132
|
+
daily commands need read access to the repository only. `fwm repo` holds the commands of its creator. Give the
|
|
133
|
+
repository with --repo-path, or once with the environment variable FWM_REPO_PATH.
|
|
134
|
+
|
|
135
|
+
\b
|
|
136
|
+
Examples:
|
|
137
|
+
# Search the repository: the checkpoints of workload bfs
|
|
138
|
+
fwm list checkpoints --workload bfs
|
|
139
|
+
|
|
140
|
+
\b
|
|
141
|
+
# The details of an entry point
|
|
142
|
+
fwm show entry bfs-road-n64
|
|
143
|
+
|
|
144
|
+
\b
|
|
145
|
+
# Simulate one SimPoint interval of a checkpoint
|
|
146
|
+
fwm mkgen from-simpoint bfs-road-n64.3.0.25 -o run1
|
|
147
|
+
make -C run1 run
|
|
148
|
+
|
|
149
|
+
\b
|
|
150
|
+
Exit status:
|
|
151
|
+
0 success 3 not found
|
|
152
|
+
1 unexpected error 4 failed check
|
|
153
|
+
2 usage error, or unusable repository
|
|
154
|
+
5 already exists (an item of the repository, or a destination)
|
|
155
|
+
"""
|
|
156
|
+
ctx.ensure_object(dict)
|
|
157
|
+
ctx.obj["repo_path"] = repo_path
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def open_repo(ctx: click.Context, write: bool = False) -> Repo:
|
|
161
|
+
"""
|
|
162
|
+
The repository given by --repo-path or FWM_REPO_PATH, opened once and closed with the command.
|
|
163
|
+
|
|
164
|
+
:param write: The command changes the repository. Otherwise it's opened read-only, so that it can be used without
|
|
165
|
+
write permission.
|
|
166
|
+
"""
|
|
167
|
+
obj = ctx.find_root().obj
|
|
168
|
+
if "repo" not in obj:
|
|
169
|
+
if not obj.get("repo_path"):
|
|
170
|
+
raise click.UsageError("no workload repository: pass --repo-path or set FWM_REPO_PATH")
|
|
171
|
+
obj["repo"] = Repo.open(obj["repo_path"], readonly=not write)
|
|
172
|
+
ctx.find_root().call_on_close(obj["repo"].close)
|
|
173
|
+
return obj["repo"]
|
|
174
|
+
|
fwm/cli/output.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from fwm.repo.query import View
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def echo_json(value: Any) -> None:
|
|
10
|
+
click.echo(json.dumps(value, indent=2))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build(build: Dict[str, str]) -> str:
|
|
14
|
+
return f"{build.get('version', '?')} ({build.get('commit', '?')[:12]})"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def summarize_provenance(provenance: View) -> View:
|
|
18
|
+
"""A trace's provenance in a few lines, for reading: when and where it was recorded, by what, and how."""
|
|
19
|
+
uname = provenance["uname"]
|
|
20
|
+
period = provenance["started"] + (f" - {provenance['finished']}" if provenance.get("finished") else
|
|
21
|
+
" (never noted as finished)")
|
|
22
|
+
summary: Dict[str, Any] = {
|
|
23
|
+
"recorded": f"{period} {provenance.get('time_zone', '')}".rstrip(),
|
|
24
|
+
"on": f"{uname['nodename']} ({uname['sysname']} {uname['release']}, {uname['machine']})",
|
|
25
|
+
}
|
|
26
|
+
simulator = provenance.get("simulator")
|
|
27
|
+
if simulator:
|
|
28
|
+
summary["simulator"] = f"{simulator['name']} {_build(simulator['build'])}"
|
|
29
|
+
summary["fesvr"] = _build(provenance["fesvr"])
|
|
30
|
+
command = provenance["command"]
|
|
31
|
+
summary["command"] = " ".join(command["argv"]) if command.get("argv") else None
|
|
32
|
+
summary["cwd"] = command["cwd"]
|
|
33
|
+
return summary
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def summarize_view(view: View) -> View:
|
|
37
|
+
"""A view shortened for reading: its build information and provenance summarized."""
|
|
38
|
+
view = summarize_sim_version(view)
|
|
39
|
+
if view.get("provenance"):
|
|
40
|
+
view = {**view, "provenance": summarize_provenance(view["provenance"])}
|
|
41
|
+
return view
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def summarize_sim_version(view: View) -> View:
|
|
45
|
+
"""A view with its `spike --version` output shortened to each component's version and commit, for reading."""
|
|
46
|
+
sim_version = view.get("sim_version")
|
|
47
|
+
if not sim_version:
|
|
48
|
+
return view
|
|
49
|
+
builds = sim_version["builds"]
|
|
50
|
+
if builds is None:
|
|
51
|
+
summary: Any = sim_version["raw"].strip()
|
|
52
|
+
else:
|
|
53
|
+
summary = {title: f"{fields['Version']} ({fields['Commit'][:12]})" for title, fields in builds.items()}
|
|
54
|
+
return {**view, "sim_version": summary}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def echo_view(view: View, indent: int = 0) -> None:
|
|
58
|
+
"""Print a view as indented "key: value" lines."""
|
|
59
|
+
pad = " " * indent
|
|
60
|
+
for key, value in view.items():
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
click.echo(f"{pad}{key}:")
|
|
63
|
+
echo_view(value, indent + 2)
|
|
64
|
+
elif isinstance(value, list) and value and isinstance(value[0], dict):
|
|
65
|
+
click.echo(f"{pad}{key}:")
|
|
66
|
+
for item in value:
|
|
67
|
+
click.echo(f"{pad} -")
|
|
68
|
+
echo_view(item, indent + 4)
|
|
69
|
+
elif isinstance(value, list):
|
|
70
|
+
click.echo(f"{pad}{key}: {', '.join(str(v) for v in value) if value else '-'}")
|
|
71
|
+
else:
|
|
72
|
+
click.echo(f"{pad}{key}: {'-' if value is None else value}")
|
|
File without changes
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`fwm repo bootstrap`: the steps that make the repository's traces and checkpoints, for its creator (in practice the
|
|
3
|
+
fwm-bootstrap pipeline). None of them writes to the repository: they generate runs and check their outputs, which
|
|
4
|
+
`fwm repo compose` then imports.
|
|
5
|
+
"""
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from fwm.cli.completion import complete_entry, complete_trace
|
|
9
|
+
from fwm.cli.fwm import open_repo, SectionedGroup
|
|
10
|
+
from fwm.cli.output import echo_json
|
|
11
|
+
from fwm.cli.subcmds.mkgen import done, out_dir_option
|
|
12
|
+
from fwm.mkgen.makefile import generate_record, generate_create_checkpoints
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group(cls=SectionedGroup, short_help="Generate and check the runs that make traces and checkpoints.",
|
|
16
|
+
sections=(("Generating the runs", ("mkgen",)), ("Checking their outputs", ("check-fesvr-trace",))))
|
|
17
|
+
def bootstrap():
|
|
18
|
+
"""
|
|
19
|
+
Generate and check the runs that make the repository's traces and checkpoints.
|
|
20
|
+
|
|
21
|
+
These commands only read the repository: `fwm repo compose` imports their results. The fwm-bootstrap pipeline
|
|
22
|
+
runs them for every app of a suite. The examples are its steps for one entry point E, with run directories R1
|
|
23
|
+
to R3.
|
|
24
|
+
|
|
25
|
+
DESC lists the checkpoints to create, and S is their SimPoint information: both come from a SimPoint analysis of
|
|
26
|
+
R1's basic block vectors (BBVs). V is the output of `spike --version`.
|
|
27
|
+
|
|
28
|
+
\b
|
|
29
|
+
Examples:
|
|
30
|
+
# 1. Record a run of E, with BBVs (-s) for SimPoint
|
|
31
|
+
fwm repo bootstrap mkgen record-fesvr-trace E -o R1
|
|
32
|
+
make -C R1 run SIM_FLAGS_EXTRA=-s<interval>
|
|
33
|
+
|
|
34
|
+
\b
|
|
35
|
+
# 2. Check the recording
|
|
36
|
+
fwm repo bootstrap check-fesvr-trace R1/trace \\
|
|
37
|
+
--final-state R1/final-state.json
|
|
38
|
+
|
|
39
|
+
\b
|
|
40
|
+
# 3. Import it as a trace: prints its UUID
|
|
41
|
+
fwm repo compose add trace E R1/trace \\
|
|
42
|
+
--final-state R1/final-state.json
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
# 4. Create the checkpoints of DESC
|
|
46
|
+
fwm repo bootstrap mkgen create-checkpoints UUID DESC -o R2
|
|
47
|
+
make -C R2 run
|
|
48
|
+
|
|
49
|
+
\b
|
|
50
|
+
# 5. Import them
|
|
51
|
+
fwm repo compose add checkpoint UUID R2/ckpt.desc R2/checkpoints \\
|
|
52
|
+
--simpoint-info S --sim-version V
|
|
53
|
+
|
|
54
|
+
\b
|
|
55
|
+
# 6. Run each checkpoint to the end
|
|
56
|
+
fwm mkgen from-checkpoint UUID:NAME -o R3
|
|
57
|
+
make -C R3 run
|
|
58
|
+
|
|
59
|
+
\b
|
|
60
|
+
# 7. Record whether the run validates the checkpoint
|
|
61
|
+
fwm repo compose set-checkpoint-validation UUID:NAME R3
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@bootstrap.group(cls=SectionedGroup, short_help="Generate a run that records a trace or makes checkpoints.",
|
|
66
|
+
sections=(("Runs, in the pipeline's order", ("record-fesvr-trace", "create-checkpoints")),))
|
|
67
|
+
def mkgen():
|
|
68
|
+
"""
|
|
69
|
+
Generate the runs that record traces and create checkpoints.
|
|
70
|
+
|
|
71
|
+
Like `fwm mkgen`, each writes a run directory, to run with `make -C DIR run`. Neither writes to the repository.
|
|
72
|
+
|
|
73
|
+
\b
|
|
74
|
+
Examples:
|
|
75
|
+
# Generate a record run, and run it
|
|
76
|
+
fwm repo bootstrap mkgen record-fesvr-trace bfs-road-n64 -o run1
|
|
77
|
+
make -C run1 run
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@mkgen.command("record-fesvr-trace", short_help="A run that records a trace of an entry point.")
|
|
82
|
+
@click.argument("entry", shell_complete=complete_entry)
|
|
83
|
+
@out_dir_option
|
|
84
|
+
@click.pass_context
|
|
85
|
+
def record_fesvr_trace(ctx, entry, out_dir):
|
|
86
|
+
"""
|
|
87
|
+
Generate a run that records a trace of an entry point, in a fresh copy of its sysroot. Linux only.
|
|
88
|
+
|
|
89
|
+
The run writes trace/ (the recording), final-state.json, and the program's console in console.stdout and
|
|
90
|
+
console.stderr. Then check the trace, and import it, as in the examples.
|
|
91
|
+
|
|
92
|
+
\b
|
|
93
|
+
Examples:
|
|
94
|
+
# Record a run, with BBVs for SimPoint
|
|
95
|
+
fwm repo bootstrap mkgen record-fesvr-trace bfs-road-n64 -o run1
|
|
96
|
+
make -C run1 run SIM_FLAGS_EXTRA=-s100000000
|
|
97
|
+
|
|
98
|
+
\b
|
|
99
|
+
# Check the trace, then import it
|
|
100
|
+
fwm repo bootstrap check-fesvr-trace run1/trace \\
|
|
101
|
+
--final-state run1/final-state.json
|
|
102
|
+
fwm repo compose add trace bfs-road-n64 run1/trace \\
|
|
103
|
+
--final-state run1/final-state.json
|
|
104
|
+
"""
|
|
105
|
+
done(generate_record(open_repo(ctx), entry, out_dir))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@mkgen.command("create-checkpoints", short_help="A run that creates the checkpoints of a description.")
|
|
109
|
+
@click.argument("trace", shell_complete=complete_trace)
|
|
110
|
+
@click.argument("desc", type=click.Path(exists=True, dir_okay=False))
|
|
111
|
+
@out_dir_option
|
|
112
|
+
@click.pass_context
|
|
113
|
+
def create_checkpoints(ctx, trace, desc, out_dir):
|
|
114
|
+
"""
|
|
115
|
+
Generate a run that replays TRACE and creates the checkpoints of DESC.
|
|
116
|
+
|
|
117
|
+
DESC lists the checkpoints, one "NAME: INSTRUCTION_COUNT" line each. The run writes ckpt.desc (a copy of DESC),
|
|
118
|
+
checkpoints/ and final-state.json.
|
|
119
|
+
|
|
120
|
+
\b
|
|
121
|
+
Examples:
|
|
122
|
+
# Create the checkpoints of ckpts.txt, then import them
|
|
123
|
+
fwm repo bootstrap mkgen create-checkpoints bfs-road-n64 ckpts.txt -o run2
|
|
124
|
+
make -C run2 run
|
|
125
|
+
fwm repo compose add checkpoint bfs-road-n64 run2/ckpt.desc run2/checkpoints
|
|
126
|
+
"""
|
|
127
|
+
done(generate_create_checkpoints(open_repo(ctx), trace, desc, out_dir))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@bootstrap.command("check-fesvr-trace", short_help="Check a recorded trace before importing it.")
|
|
131
|
+
@click.argument("recording_dir", type=click.Path(file_okay=False))
|
|
132
|
+
@click.option("--final-state", type=click.Path(dir_okay=False),
|
|
133
|
+
help="The run's final state (final-state.json).")
|
|
134
|
+
@click.option("--json", "as_json", is_flag=True,
|
|
135
|
+
help="Print JSON, for scripts.")
|
|
136
|
+
def check_fesvr_trace(recording_dir, final_state, as_json):
|
|
137
|
+
"""
|
|
138
|
+
Check a recording before importing it, as `fwm repo compose add trace` will.
|
|
139
|
+
|
|
140
|
+
It needs no repository: run it on the node that made the recording. It passes when the recording is complete and
|
|
141
|
+
well formed, and, with --final-state, when the run ended with the program's exit. It prints the exit status and
|
|
142
|
+
each processor's (HART's) instruction count.
|
|
143
|
+
|
|
144
|
+
It fails with status 4 otherwise, e.g. for a run stopped by a job's time limit, which is worth retrying with more
|
|
145
|
+
time.
|
|
146
|
+
|
|
147
|
+
\b
|
|
148
|
+
Examples:
|
|
149
|
+
# Check the trace of a record run, on the node that ran it
|
|
150
|
+
fwm repo bootstrap check-fesvr-trace run1/trace \\
|
|
151
|
+
--final-state run1/final-state.json
|
|
152
|
+
"""
|
|
153
|
+
# imported here: it loads pydantic, which completing `fwm repo ...` must not
|
|
154
|
+
from fwm.repo.trace import check_recording
|
|
155
|
+
|
|
156
|
+
recording, state = check_recording(recording_dir, final_state)
|
|
157
|
+
instrets = state.instrets if state is not None else None
|
|
158
|
+
harts = [{"hart_id": h.hart_id, "recording_sha256": h.recording_sha256, "num_commands": h.num_commands,
|
|
159
|
+
"instret": instrets[h.hart_id] if instrets is not None else None} for h in recording.harts]
|
|
160
|
+
if as_json:
|
|
161
|
+
spec = recording.target_spec
|
|
162
|
+
echo_json({"complete": True, "exit_status": recording.exit_status, "mem_mb": spec.mem_mb,
|
|
163
|
+
"num_harts": spec.num_harts, "boot_elf_sha256": spec.boot_elf_sha256, "harts": harts})
|
|
164
|
+
else:
|
|
165
|
+
instret = "unknown (no final state)" if instrets is None else str(instrets)
|
|
166
|
+
click.echo(f"complete: the program exited with status {recording.exit_status}; instret {instret}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click.shell_completion import get_completion_class
|
|
3
|
+
|
|
4
|
+
COMPLETE_VAR = "_FWM_COMPLETE"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.command()
|
|
8
|
+
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"]))
|
|
9
|
+
@click.pass_context
|
|
10
|
+
def completion(ctx, shell):
|
|
11
|
+
"""
|
|
12
|
+
Print the shell completion script.
|
|
13
|
+
|
|
14
|
+
Completion offers fwm's commands and options, and the names in the repository of --repo-path or FWM_REPO_PATH.
|
|
15
|
+
Enable it in your shell's startup file, as in the examples. fwm must be on PATH.
|
|
16
|
+
|
|
17
|
+
\b
|
|
18
|
+
Examples:
|
|
19
|
+
# zsh: in ~/.zshrc
|
|
20
|
+
eval "$(fwm completion zsh)"
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
# bash: in ~/.bashrc
|
|
24
|
+
eval "$(fwm completion bash)"
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
# fish: in ~/.config/fish/config.fish
|
|
28
|
+
fwm completion fish | source
|
|
29
|
+
"""
|
|
30
|
+
completion_class = get_completion_class(shell)
|
|
31
|
+
assert completion_class is not None
|
|
32
|
+
click.echo(completion_class(ctx.find_root().command, {}, "fwm", COMPLETE_VAR).source(), nl=False)
|