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.
Files changed (69) hide show
  1. fwm/__init__.py +0 -0
  2. fwm/__main__.py +3 -0
  3. fwm/cli/__init__.py +0 -0
  4. fwm/cli/completion.py +191 -0
  5. fwm/cli/fwm.py +174 -0
  6. fwm/cli/output.py +72 -0
  7. fwm/cli/subcmds/__init__.py +0 -0
  8. fwm/cli/subcmds/bootstrap.py +166 -0
  9. fwm/cli/subcmds/completion.py +32 -0
  10. fwm/cli/subcmds/compose.py +280 -0
  11. fwm/cli/subcmds/list.py +96 -0
  12. fwm/cli/subcmds/mkgen.py +141 -0
  13. fwm/cli/subcmds/repo.py +263 -0
  14. fwm/cli/subcmds/show.py +56 -0
  15. fwm/cli/subcmds/spawn.py +39 -0
  16. fwm/data_loader/__init__.py +0 -0
  17. fwm/data_loader/checkpoint.py +192 -0
  18. fwm/data_loader/ckpt_desc.py +69 -0
  19. fwm/data_loader/entry_point.py +52 -0
  20. fwm/data_loader/file_catalog.py +76 -0
  21. fwm/data_loader/final_state.py +114 -0
  22. fwm/data_loader/provenance.py +125 -0
  23. fwm/data_loader/recording.py +266 -0
  24. fwm/data_loader/sim_version.py +67 -0
  25. fwm/data_loader/simpoint_info.py +43 -0
  26. fwm/database/__init__.py +0 -0
  27. fwm/database/catalog_store.py +69 -0
  28. fwm/database/custom_field.py +24 -0
  29. fwm/database/db.py +80 -0
  30. fwm/database/schema.py +335 -0
  31. fwm/mkgen/__init__.py +0 -0
  32. fwm/mkgen/makefile.py +304 -0
  33. fwm/mkgen/run_json.py +45 -0
  34. fwm/mkgen/templates/create-checkpoints.mk +15 -0
  35. fwm/mkgen/templates/from-checkpoint.mk +16 -0
  36. fwm/mkgen/templates/from-fesvr-trace.mk +13 -0
  37. fwm/mkgen/templates/from-simpoint.mk +18 -0
  38. fwm/mkgen/templates/from-sysroot.mk +28 -0
  39. fwm/mkgen/templates/header.mk +27 -0
  40. fwm/mkgen/templates/record.mk +28 -0
  41. fwm/py.typed +0 -0
  42. fwm/repo/__init__.py +0 -0
  43. fwm/repo/checkpoint.py +220 -0
  44. fwm/repo/entry.py +139 -0
  45. fwm/repo/errors.py +24 -0
  46. fwm/repo/gc.py +134 -0
  47. fwm/repo/listing.py +133 -0
  48. fwm/repo/lookup.py +102 -0
  49. fwm/repo/query.py +136 -0
  50. fwm/repo/remove.py +81 -0
  51. fwm/repo/repo.py +218 -0
  52. fwm/repo/spawn.py +118 -0
  53. fwm/repo/subrepo.py +63 -0
  54. fwm/repo/sysroot.py +80 -0
  55. fwm/repo/trace.py +218 -0
  56. fwm/repo/transfer.py +567 -0
  57. fwm/repo/validation.py +73 -0
  58. fwm/utils/__init__.py +0 -0
  59. fwm/utils/digest.py +44 -0
  60. fwm/utils/file_catalog.py +213 -0
  61. fwm/utils/fs.py +149 -0
  62. fwm/utils/log.py +77 -0
  63. fwm/utils/target_path.py +38 -0
  64. fwm/version.py +46 -0
  65. fwm-0.1.1.dist-info/METADATA +353 -0
  66. fwm-0.1.1.dist-info/RECORD +69 -0
  67. fwm-0.1.1.dist-info/WHEEL +4 -0
  68. fwm-0.1.1.dist-info/entry_points.txt +3 -0
  69. fwm-0.1.1.dist-info/licenses/LICENSE +33 -0
fwm/__init__.py ADDED
File without changes
fwm/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from fwm.cli.fwm import cli
2
+
3
+ cli()
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)