hector-cli 0.1.0__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.
hector/modules.py ADDED
@@ -0,0 +1,201 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """
3
+ MODULE_KINDS — how a module type is built into a loadable artifact (.so or .dll).
4
+
5
+ Register a builder `(module_name, module_config, BuildContext) -> artifact_host_path`:
6
+ @MODULE_KINDS.register("my-kind")
7
+ def build_my_kind(module_name, cfg, ctx):
8
+ return path_to_artifact
9
+ """
10
+
11
+ import glob
12
+ import os
13
+ import shutil
14
+ from dataclasses import dataclass
15
+
16
+ from .core import (ARTIFACTS_DIR, LIBOPENLIBM_NAME, Registry,
17
+ container_path_for, split_host_container, to_container_path)
18
+ from .docker import run_build
19
+
20
+ MODULE_KINDS = Registry("module kind")
21
+
22
+
23
+ @dataclass
24
+ class BuildContext:
25
+ renode_dir_container: str
26
+ integration_dir_host: str
27
+ image: str
28
+
29
+
30
+ def _libopenlibm_flag(integration_dir_host):
31
+ libm = os.path.join(integration_dir_host, "lib", LIBOPENLIBM_NAME)
32
+ if os.path.isfile(libm):
33
+ return f'-DLIBOPENLIBM="{container_path_for(libm)}"'
34
+ print(f" [!] {LIBOPENLIBM_NAME} not found at {libm}; skipping -DLIBOPENLIBM "
35
+ "(add it via cmake_flags if your build needs it).")
36
+ return ""
37
+
38
+
39
+ @MODULE_KINDS.register("renode-verilator")
40
+ def build_renode_verilator(module_name, module_config, ctx):
41
+ """Build a verilated co-simulation peripheral into a .so, inside the Renode image."""
42
+ if not module_config.get("type"):
43
+ raise ValueError(f"Module '{module_name}' is missing required 'type' (its Renode class).")
44
+ source = module_config.get("source")
45
+ if not source:
46
+ raise ValueError(f"Module '{module_name}' is missing required 'source'.")
47
+
48
+ source_host, source_container = split_host_container(source)
49
+ if not os.path.isdir(source_host):
50
+ raise FileNotFoundError(f"Module '{module_name}': source dir not found: {source} ({source_host})")
51
+
52
+ build_host = os.path.join(ARTIFACTS_DIR, "build", "modules", module_name)
53
+ os.makedirs(build_host, exist_ok=True)
54
+ build_container = to_container_path(build_host)
55
+ cmake_flags = module_config.get("cmake_flags", "")
56
+ auto_flags = _libopenlibm_flag(ctx.integration_dir_host)
57
+ uid, gid = os.getuid(), os.getgid()
58
+
59
+ print(f"\n[MODULE] Building '{module_name}' ({module_config['type']}) in {ctx.image}")
60
+ print(f" source: {source}")
61
+
62
+ run_build(
63
+ "set -e\n"
64
+ "export DEBIAN_FRONTEND=noninteractive\n"
65
+ "apt-get update\n"
66
+ "apt-get install -y --no-install-recommends build-essential cmake verilator\n"
67
+ f'cmake -S "{source_container}" -B "{build_container}" '
68
+ f'-DUSER_RENODE_DIR="{ctx.renode_dir_container}" {auto_flags} {cmake_flags}\n'
69
+ f'cmake --build "{build_container}"\n'
70
+ f'chown -R {uid}:{gid} "{build_container}"\n',
71
+ ctx.image,
72
+ )
73
+
74
+ so_files = glob.glob(os.path.join(build_host, "**", "*.so"), recursive=True)
75
+ if not so_files:
76
+ raise RuntimeError(f"Module '{module_name}' produced no .so in {build_host}.")
77
+ if len(so_files) > 1:
78
+ raise RuntimeError(f"Module '{module_name}' produced multiple .so files: {so_files}.")
79
+ print(f" -> {so_files[0]}")
80
+ return so_files[0]
81
+
82
+
83
+ # Renode DLLs bundled in its own installation — filter these out when locating
84
+ # the user's output DLL after a dotnet build.
85
+ _RENODE_DLL_PREFIXES = (
86
+ "Antmicro.", "Mono.", "System.", "Microsoft.", "mscorlib",
87
+ "netstandard", "IronPython", "FasterReflection",
88
+ )
89
+
90
+
91
+ @MODULE_KINDS.register("csharp")
92
+ def build_csharp(module_name, module_config, ctx):
93
+ """Build or locate a C# Renode peripheral plugin (.dll).
94
+ `source` can be a pre-built .dll path or a directory containing a .csproj.
95
+ The DLL is loaded at emulation scope via `i @<path>`; one import covers all instances.
96
+ """
97
+ if not module_config.get("type"):
98
+ raise ValueError(f"Module '{module_name}' is missing required 'type' (the C# class name).")
99
+ source = module_config.get("source")
100
+ if not source:
101
+ raise ValueError(f"Module '{module_name}' is missing required 'source'.")
102
+
103
+ source_host, _ = split_host_container(source)
104
+
105
+ # ---- pre-built DLL: use as-is ----
106
+ if source_host.endswith(".dll"):
107
+ if not os.path.isfile(source_host):
108
+ raise FileNotFoundError(f"Module '{module_name}': DLL not found: {source} ({source_host})")
109
+ print(f"\n[MODULE] C# '{module_name}' ({module_config['type']}) using pre-built DLL")
110
+ print(f" source: {source}")
111
+ return source_host
112
+
113
+ # ---- build from source ----
114
+ if not os.path.isdir(source_host):
115
+ raise FileNotFoundError(
116
+ f"Module '{module_name}': source not found: {source} ({source_host})\n"
117
+ " Provide either a directory containing a .csproj or a path to a pre-built .dll."
118
+ )
119
+
120
+ source_container = to_container_path(source_host)
121
+ build_host = os.path.join(ARTIFACTS_DIR, "build", "modules", module_name)
122
+ os.makedirs(build_host, exist_ok=True)
123
+ build_container = to_container_path(build_host)
124
+ uid, gid = os.getuid(), os.getgid()
125
+
126
+ print(f"\n[MODULE] Building C# '{module_name}' ({module_config['type']}) in {ctx.image}")
127
+ print(f" source: {source}")
128
+
129
+ run_build(
130
+ "set -e\n"
131
+ # Renode's official image ships Mono; dotnet may also be present.
132
+ # Try dotnet first (produces a self-contained output), fall back to msbuild/xbuild.
133
+ f'if command -v dotnet >/dev/null 2>&1; then\n'
134
+ f' dotnet build "{source_container}" -o "{build_container}" -c Release\n'
135
+ f'elif command -v msbuild >/dev/null 2>&1; then\n'
136
+ f' msbuild "{source_container}" /p:OutputPath="{build_container}" /p:Configuration=Release\n'
137
+ f'else\n'
138
+ f' echo "[ERROR] Neither dotnet nor msbuild found in container {ctx.image}"; exit 1\n'
139
+ f'fi\n'
140
+ f'chown -R {uid}:{gid} "{build_container}"\n',
141
+ ctx.image,
142
+ )
143
+
144
+ # Collect DLLs, excluding Renode/framework assemblies
145
+ all_dlls = glob.glob(os.path.join(build_host, "**", "*.dll"), recursive=True)
146
+ user_dlls = [
147
+ f for f in all_dlls
148
+ if not any(os.path.basename(f).startswith(p) for p in _RENODE_DLL_PREFIXES)
149
+ ]
150
+
151
+ if not user_dlls:
152
+ raise RuntimeError(
153
+ f"Module '{module_name}': no user DLL found in {build_host}.\n"
154
+ f" All .dll files found: {[os.path.basename(f) for f in all_dlls]}"
155
+ )
156
+ if len(user_dlls) > 1:
157
+ # Try to pick the one whose name matches the module or the last path component of source
158
+ stem = os.path.basename(source_host)
159
+ candidates = [f for f in user_dlls
160
+ if module_name.lower() in os.path.basename(f).lower()
161
+ or stem.lower() in os.path.basename(f).lower()]
162
+ if len(candidates) == 1:
163
+ user_dlls = candidates
164
+ else:
165
+ raise RuntimeError(
166
+ f"Module '{module_name}': multiple user DLLs found — cannot determine which "
167
+ f"to use: {[os.path.basename(f) for f in user_dlls]}.\n"
168
+ f" Rename your project output or point 'source' directly at the .dll."
169
+ )
170
+
171
+ print(f" -> {user_dlls[0]}")
172
+ return user_dlls[0]
173
+
174
+
175
+ def build_modules(modules_cfg, ctx):
176
+ """Build every declared module via its registered kind.
177
+
178
+ Returns {module_name: {'type': renode_class, 'artifact_host': path, 'artifact_type': 'so'|'dll'}}
179
+ """
180
+ registry = {}
181
+ for name, cfg in (modules_cfg or {}).items():
182
+ kind = cfg.get("kind", "renode-verilator")
183
+ builder = MODULE_KINDS.get(kind)
184
+ artifact_host = builder(name, cfg, ctx)
185
+ artifact_type = "dll" if artifact_host.endswith(".dll") else "so"
186
+ registry[name] = {
187
+ "type": cfg.get("type"),
188
+ "artifact_host": artifact_host,
189
+ "artifact_type": artifact_type,
190
+ }
191
+ return registry
192
+
193
+
194
+ def materialize_instance_so(node_name, instance, module_so_host):
195
+ """Copy a module's .so to a per-instance file so each instance gets a distinct inode
196
+ (and therefore independent simulation state when loaded)."""
197
+ inst_dir = os.path.join(ARTIFACTS_DIR, "build", "instances")
198
+ os.makedirs(inst_dir, exist_ok=True)
199
+ dest = os.path.join(inst_dir, f"{node_name}__{instance}.so")
200
+ shutil.copy2(module_so_host, dest)
201
+ return dest
hector/peripherals.py ADDED
@@ -0,0 +1,168 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Peripheral instantiation: place a native Renode class OR a module type into a machine.
3
+
4
+ Verilator (.so) modules: per-instance .so copy + SimulationFilePathLinux command
5
+ (stored in NodeDescriptor.so_commands).
6
+ C# (.dll) modules: emulation-level `i @<dll>` import (stored in
7
+ NodeDescriptor.csharp_dlls; pipeline.py emits these before
8
+ any machine block in the resc).
9
+ """
10
+
11
+ import os
12
+
13
+ from .core import to_container_path
14
+ from .generator import NodeDescriptor, PeripheralDef
15
+ from .modules import materialize_instance_so
16
+
17
+ _RESERVED = {"type", "at", "peripherals", "init", "connections"}
18
+
19
+
20
+ def _parse_repl_signals(raw):
21
+ """Parse repl-style signal connection lines into {peripheral_name: [repl_line, ...]}.
22
+
23
+ Each line: <periph>[<pin_range>] -> <target>@<pin>
24
+ nvic -> cpu@0 => nvic: ["-> cpu@0"]
25
+ nvic.IRQ -> cpu@0 => nvic: ["IRQ -> cpu@0"]
26
+ gpioPortC[0-15] -> nvic@6 => gpioPortC: ["[0-15] -> nvic@6"]
27
+
28
+ The destination (right of '->') is preserved verbatim, so Renode's receiver
29
+ qualifiers pass through untouched, e.g. a local index '#N' and an input pin '@N':
30
+ timer1@0 -> gpioPortA#08@01 => timer1: ["0 -> gpioPortA#08@01"]
31
+ The source (left of '->') is only a named signal, a number, or a range — '#' is a
32
+ receiver-only qualifier, so a '#' on the source side is flagged, not emitted.
33
+ """
34
+ result = {}
35
+ for line in (raw or "").splitlines():
36
+ line = line.strip()
37
+ if not line or line.startswith("#"):
38
+ continue
39
+ if " -> " not in line:
40
+ print(f" [WARN] irqs: '{line}' has no '->' — skipped.")
41
+ continue
42
+ source, target = line.split(" -> ", 1)
43
+ source, target = source.strip(), target.strip()
44
+ if "#" in source:
45
+ # '#localIndex' is a Renode *receiver* qualifier (it belongs on the
46
+ # destination, right of '->'). It has no meaning on the source side, so
47
+ # emitting it would produce a .repl Renode rejects. Flag and skip.
48
+ print(f" [WARN] irqs: '{line}' has '#' on the source side; '#index' is a "
49
+ "destination (receiver) qualifier and belongs after '->' "
50
+ "(e.g. 'timer1@0 -> gpioPortA#08@01'). Skipped.")
51
+ continue
52
+ if "[" in source:
53
+ periph, pin = source.split("[", 1)
54
+ irq_line = f"[{pin} -> {target}"
55
+ elif "." in source:
56
+ periph, signal = source.split(".", 1)
57
+ irq_line = f"{signal} -> {target}"
58
+ elif "@" in source:
59
+ periph, pin = source.split("@", 1)
60
+ irq_line = f"{pin} -> {target}"
61
+ else:
62
+ periph = source
63
+ irq_line = f"-> {target}"
64
+ result.setdefault(periph.strip(), []).append(irq_line)
65
+ return result
66
+
67
+
68
+ def _gather_connections(peripherals):
69
+ """Collect `connections:` blocks declared on peripherals (recursively).
70
+
71
+ Lets wiring live next to the peripheral it concerns — e.g. an `mcu` grouping
72
+ peripheral can carry all the MCU-internal wiring, separate from a board-level
73
+ `connections:` at the node. Returns the combined raw text, parsed together with
74
+ the node-level connections. These are always intra-machine signals; cross-machine
75
+ hub links must still use the node-level `connections:` block.
76
+ """
77
+ blocks = []
78
+ for spec in (peripherals or {}).values():
79
+ spec = spec or {}
80
+ if not isinstance(spec, dict):
81
+ continue
82
+ conn = spec.get("connections")
83
+ if conn:
84
+ blocks.append(conn if isinstance(conn, str) else "\n".join(str(c) for c in conn))
85
+ child = spec.get("peripherals")
86
+ if child:
87
+ sub = _gather_connections(child)
88
+ if sub:
89
+ blocks.append(sub)
90
+ return "\n".join(blocks)
91
+
92
+
93
+ def resolve_peripherals(concrete_nodes, modules_registry):
94
+ """Build a NodeDescriptor for each renode node.
95
+
96
+ Returns {node_name: NodeDescriptor} with peripheral_defs, so_commands,
97
+ csharp_dlls, and init populated. updating_entries is left empty — connections.py
98
+ fills those in later.
99
+ """
100
+ descriptors = {}
101
+ for node_name, node_config in concrete_nodes.items():
102
+ if node_config.get("backend", "renode") != "renode":
103
+ continue
104
+ # Wiring may be declared at the node level (`connections:`) or inline on a
105
+ # peripheral (a `connections:` block on the peripheral it concerns). Both are
106
+ # intra-machine signals and merge into one map.
107
+ raw_signals = node_config.get("_repl_signals", "")
108
+ periph_signals = _gather_connections(node_config.get("peripherals") or {})
109
+ signals_map = _parse_repl_signals(
110
+ "\n".join(s for s in (raw_signals, periph_signals) if s))
111
+ init = (node_config.get("init") or "").strip() or None
112
+ descriptor = NodeDescriptor(name=node_name, init=init)
113
+
114
+ for instance, spec in (node_config.get("peripherals") or {}).items():
115
+ _resolve(instance, spec or {}, node_name, descriptor, modules_registry,
116
+ default_at="sysbus", signals_map=signals_map)
117
+
118
+ # Signals targeting peripherals not declared in this YAML (from a platform .repl file)
119
+ # can't be inlined; emit them as updating entries instead.
120
+ declared = {p.name for p in descriptor.peripheral_defs}
121
+ for periph_name, wires in signals_map.items():
122
+ if periph_name not in declared:
123
+ for wire in wires:
124
+ descriptor.updating_entries.append((periph_name, wire))
125
+
126
+ descriptors[node_name] = descriptor
127
+ return descriptors
128
+
129
+
130
+ def _resolve(instance, spec, node_name, descriptor, modules_registry, default_at, signals_map=None):
131
+ type_name = spec.get("type")
132
+ if not type_name:
133
+ raise ValueError(f"peripherals.{instance} on '{node_name}' is missing 'type'.")
134
+
135
+ at = spec["at"] if "at" in spec else default_at
136
+ at = at or None
137
+ signals = list((signals_map or {}).get(instance, []))
138
+ init = spec.get("init") or None
139
+ args = {k: v for k, v in spec.items() if k not in _RESERVED}
140
+
141
+ if type_name in modules_registry:
142
+ mod = modules_registry[type_name]
143
+ descriptor.peripheral_defs.append(
144
+ PeripheralDef(name=instance, ptype=mod["type"], at=at,
145
+ args=args, signals=signals, init=init))
146
+
147
+ if mod["artifact_type"] == "so":
148
+ inst_so = materialize_instance_so(node_name, instance, mod["artifact_host"])
149
+ descriptor.so_commands.append(
150
+ f"{instance} SimulationFilePathLinux @{to_container_path(inst_so)}")
151
+ print(f"[INST] {node_name}.{instance} = module '{type_name}' ({mod['type']})"
152
+ + (f" @ {at}" if at else "")
153
+ + f" [.so: {os.path.basename(inst_so)}]")
154
+ else:
155
+ descriptor.csharp_dlls.add(mod["artifact_host"])
156
+ print(f"[INST] {node_name}.{instance} = C# module '{type_name}' ({mod['type']})"
157
+ + (f" @ {at}" if at else "")
158
+ + f" [.dll: {os.path.basename(mod['artifact_host'])}]")
159
+ else:
160
+ descriptor.peripheral_defs.append(
161
+ PeripheralDef(name=instance, ptype=type_name, at=at,
162
+ args=args, signals=signals, init=init))
163
+ print(f"[INST] {node_name}.{instance} = native {type_name} "
164
+ + (f"@ {at}" if at else "(no registration)"))
165
+
166
+ for child_instance, child_spec in (spec.get("peripherals") or {}).items():
167
+ _resolve(child_instance, child_spec or {}, node_name, descriptor, modules_registry,
168
+ default_at=instance, signals_map=signals_map)