Coverage for apio/scons/plugin_base.py: 91%
64 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
1# -*- coding: utf-8 -*-
2# -- This file is part of the Apio project
3# -- (C) 2016-2018 FPGAwars
4# -- Author Jesús Arroyo
5# -- License GPLv2
6# -- Derived from:
7# ---- Platformio project
8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com>
9# ---- License Apache v2
11"""Apio scons related utilities.."""
13from pathlib import Path
14from dataclasses import dataclass
15from typing import List, cast
16import webbrowser
17from SCons.Builder import BuilderBase, CompositeBuilder
18from SCons.Action import Action
19from SCons.Script import Builder
20from SCons.Node.FS import File
21from SCons.Script.SConscript import SConsEnvironment
23# from SCons.Node.Alias import Alias
24from apio.common.apio_console import cout
25from apio.common.apio_styles import SUCCESS
26from apio.common.common_util import SRC_SUFFIXES
27from apio.scons.apio_env import ApioEnv
28from apio.common.proto.apio_pb2 import GraphOutputType
29from apio.scons.plugin_util import (
30 verilog_src_scanner,
31 get_constraint_file,
32 get_define_flags,
33)
36# -- Supported apio graph types.
37SUPPORTED_GRAPH_TYPES = ["svg", "pdf", "png"]
40@dataclass(frozen=True)
41class ArchPluginInfo:
42 """Provides information about the plugin."""
44 # -- The suffix of the constraint file.
45 constrains_file_suffix: str
46 # -- The suffix of the nextpnr generated file.
47 pnr_file_suffix: str
48 # -- The suffix of the bitstream file.
49 bitstream_file_suffix: str
50 # -- Index of the significant term when parsing clock names.
51 clk_name_index: int
54class PluginBase:
55 """Base apio arch plugin handler"""
57 def __init__(self, apio_env: ApioEnv):
58 self.apio_env = apio_env
60 # -- Scanner for verilog source files.
61 self.verilog_src_scanner = verilog_src_scanner(apio_env)
63 # -- A placeholder for the constraint file name.
64 self._constrain_file: str | None = None
66 def plugin_info(self) -> ArchPluginInfo: # pragma: no cover
67 """Return plugin specific parameters."""
68 raise NotImplementedError("Implement in subclass.")
70 def constrain_file(self) -> str:
71 """Finds and returns the constraint file path."""
72 # -- Keep short references.
73 apio_env = self.apio_env
75 # -- On first call, determine and cache.
76 if self._constrain_file is None:
77 self._constrain_file = get_constraint_file(
78 apio_env, self.plugin_info().constrains_file_suffix
79 )
80 return self._constrain_file
82 def synth_builder(self) -> BuilderBase: # pragma: no cover
83 """Creates and returns the synth builder."""
84 raise NotImplementedError("Implement in subclass.")
86 def pnr_builder(self) -> BuilderBase: # pragma: no cover
87 """Creates and returns the pnr builder."""
88 raise NotImplementedError("Implement in subclass.")
90 def bitstream_builder(self) -> BuilderBase: # pragma: no cover
91 """Creates and returns the bitstream builder."""
92 raise NotImplementedError("Implement in subclass.")
94 def testbench_compile_builder(self) -> BuilderBase: # pragma: no cover
95 """Creates and returns the testbench compile builder."""
96 raise NotImplementedError("Implement in subclass.")
98 def testbench_run_builder(self) -> BuilderBase | CompositeBuilder:
99 """Creates and returns the testbench run builder."""
101 # -- Sanity checks
102 assert self.apio_env.targeting_one_of("sim", "test")
103 assert self.apio_env.params.target.HasField(
104 "sim"
105 ) or self.apio_env.params.target.HasField("test")
107 return Builder(
108 action="vvp $SOURCE -dumpfile=$TARGET",
109 suffix=".vcd",
110 src_suffix=".out",
111 )
113 def yosys_dot_builder(self) -> BuilderBase | CompositeBuilder:
114 """Creates and returns the yosys dot builder. Should be called
115 only when serving the graph command."""
117 # -- Sanity checks
118 assert self.apio_env.targeting_one_of("graph")
119 assert self.apio_env.params.target.HasField("graph")
121 # -- Shortcuts.
122 apio_env = self.apio_env
123 params = apio_env.params
124 graph_params = params.target.graph
126 # -- Determine top module value. First priority is to the
127 # -- graph cmd param.
128 top_module = (
129 graph_params.top_module
130 if graph_params.top_module
131 else params.apio_env_params.top_module
132 )
134 return Builder(
135 # See https://tinyurl.com/yosys-sv-graph
136 # For -wireshape see https://github.com/YosysHQ/yosys/pull/4252
137 action=(
138 'yosys -p "read_verilog -sv $SOURCES; show -format dot'
139 ' -colors 1 -wireshape plaintext -prefix {0} {1}" '
140 "-DSYNTHESIZE {2} {3}"
141 ).format(
142 apio_env.graph_target,
143 top_module,
144 "" if params.verbosity.all else "-q",
145 get_define_flags(apio_env),
146 ),
147 suffix=".dot",
148 src_suffix=SRC_SUFFIXES,
149 source_scanner=self.verilog_src_scanner,
150 )
152 def graphviz_renderer_builder(self) -> BuilderBase:
153 """Creates and returns the graphviz renderer builder. Should
154 be called only when serving the graph command."""
156 # -- Sanity checks.
157 assert self.apio_env.targeting_one_of("graph")
158 assert self.apio_env.params.target.HasField("graph")
160 # -- Shortcuts.
161 apio_env = self.apio_env
162 params = apio_env.params
163 graph_params = params.target.graph
165 # -- Determine the output type string.
166 type_map = {
167 GraphOutputType.PDF: "pdf",
168 GraphOutputType.PNG: "png",
169 GraphOutputType.SVG: "svg",
170 }
171 type_str = type_map[graph_params.output_type]
172 assert type_str, f"Unexpected graph type {graph_params.output_type}"
174 def completion_action(
175 target: List[File],
176 source: List[File],
177 env: SConsEnvironment,
178 ): # noqa
179 """Action function that prints a completion message and if
180 requested, open a viewer on the output file.."""
181 _ = (source, env) # Unused
182 # -- Get the rendered file.
183 target_file: File = target[0]
184 assert isinstance(target_file, File)
185 # -- Print a message
186 cout(f"Generated {str(target_file)}", style=SUCCESS)
187 # -- If requested, convert the file to URI and open it in the
188 # -- default browser.
189 if graph_params.open_viewer: 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true
190 cout("Opening default browser")
191 file_path = Path(target_file.get_abspath())
192 file_uri = file_path.resolve().as_uri()
193 default_browser = webbrowser.get()
194 default_browser.open(file_uri)
195 else:
196 cout("User requested no graph viewer")
198 actions = [
199 f"dot -T{type_str} $SOURCES -o $TARGET",
200 Action(completion_action, "completion_action"),
201 ]
203 graphviz_builder = cast(
204 BuilderBase,
205 Builder(
206 # Expecting graphviz dot to be installed and in the path.
207 action=actions,
208 suffix=f".{type_str}",
209 src_suffix=".dot",
210 ),
211 )
213 return graphviz_builder
215 def lint_config_builder(self) -> BuilderBase: # pragma: no cover
216 """Creates and returns the lint config builder."""
217 raise NotImplementedError("Implement in subclass.")
219 def lint_builder(self) -> BuilderBase: # pragma: no cover
220 """Creates and returns the lint builder."""
221 raise NotImplementedError("Implement in subclass.")