Coverage for apio/scons/plugin_base.py: 91%
64 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""Apio scons related utilities.."""
11from pathlib import Path
12from dataclasses import dataclass
13from typing import cast
14import webbrowser
15from SCons.Builder import BuilderBase, CompositeBuilder
16from SCons.Action import Action
17from SCons.Script import Builder
18from SCons.Node.FS import File
19from SCons.Script.SConscript import SConsEnvironment
21# from SCons.Node.Alias import Alias
22from apio.common.apio_console import cout
23from apio.common.apio_styles import SUCCESS
24from apio.common.common_util import SRC_SUFFIXES
25from apio.scons.apio_env import ApioEnv
26from apio.common.proto.stubs.apio_scons_pb2 import GraphOutputType
27from apio.scons.plugin_util import (
28 verilog_src_scanner,
29 get_constraint_file,
30 get_define_flags,
31)
34# -- Supported apio graph types.
35SUPPORTED_GRAPH_TYPES = ["svg", "pdf", "png"]
38@dataclass(frozen=True)
39class ArchPluginInfo:
40 """Provides information about the plugin."""
42 # -- The suffix of the constraint file.
43 constrains_file_suffix: str
44 # -- The suffix of the nextpnr generated file.
45 pnr_file_suffix: str
46 # -- The suffix of the bitstream file.
47 bitstream_file_suffix: str
50class PluginBase:
51 """Base apio arch plugin handler"""
53 def __init__(self, apio_env: ApioEnv):
54 self.apio_env = apio_env
56 # -- Scanner for verilog source files.
57 self.verilog_src_scanner = verilog_src_scanner(apio_env)
59 # -- A placeholder for the constraint file name.
60 self._constrain_file: str | None = None
62 def plugin_info(self) -> ArchPluginInfo: # pragma: no cover
63 """Return plugin specific parameters."""
64 raise NotImplementedError("Implement in subclass.")
66 def constrain_file(self) -> str:
67 """Finds and returns the constraint file path."""
68 # -- Keep short references.
69 apio_env = self.apio_env
71 # -- On first call, determine and cache.
72 if self._constrain_file is None:
73 self._constrain_file = get_constraint_file(
74 apio_env, self.plugin_info().constrains_file_suffix
75 )
76 return self._constrain_file
78 def make_synth_builder(self) -> BuilderBase: # pragma: no cover
79 """Creates and returns the synth builder."""
80 raise NotImplementedError("Implement in subclass.")
82 def make_pnr_builder(self) -> BuilderBase: # pragma: no cover
83 """Creates and returns the pnr builder."""
84 raise NotImplementedError("Implement in subclass.")
86 def make_bitstream_builder(self) -> BuilderBase: # pragma: no cover
87 """Creates and returns the bitstream builder."""
88 raise NotImplementedError("Implement in subclass.")
90 def make_testbench_compile_builder(
91 self,
92 ) -> BuilderBase: # pragma: no cover
93 """Creates and returns the testbench compile builder."""
94 raise NotImplementedError("Implement in subclass.")
96 def testbench_run_builder(self) -> BuilderBase | CompositeBuilder:
97 """Creates and returns the testbench run builder."""
99 # -- Sanity checks
100 assert self.apio_env.targeting_one_of("sim", "test")
101 assert self.apio_env.params.target.HasField(
102 "sim"
103 ) or self.apio_env.params.target.HasField("test")
105 return Builder(
106 action="vvp $SOURCE -dumpfile=$TARGET",
107 src_suffix=".out",
108 suffix=".vcd",
109 )
111 def yosys_dot_builder(self) -> BuilderBase | CompositeBuilder:
112 """Creates and returns the yosys dot builder. Should be called
113 only when serving the graph command."""
115 # -- Sanity checks
116 assert self.apio_env.targeting_one_of("graph")
117 assert self.apio_env.params.target.HasField("graph")
119 # -- Shortcuts.
120 apio_env = self.apio_env
121 params = apio_env.params
122 graph_params = params.target.graph
124 # -- Determine top module value. First priority is to the
125 # -- graph cmd param.
126 top_module = (
127 graph_params.top_module
128 if graph_params.top_module
129 else params.apio_env_params.top_module
130 )
132 return Builder(
133 # See https://tinyurl.com/yosys-sv-graph
134 # For -wireshape see https://github.com/YosysHQ/yosys/pull/4252
135 action=(
136 'yosys -p "read_verilog -sv $SOURCES; show -format dot'
137 ' -colors 1 -wireshape plaintext -prefix {0} {1}" '
138 "-DSYNTHESIZE {2} {3}"
139 ).format(
140 apio_env.graph_target,
141 top_module,
142 "" if params.verbosity.all else "-q",
143 get_define_flags(apio_env),
144 ),
145 source_scanner=self.verilog_src_scanner,
146 src_suffix=SRC_SUFFIXES,
147 suffix=".dot",
148 )
150 def graphviz_renderer_builder(self) -> BuilderBase:
151 """Creates and returns the graphviz renderer builder. Should
152 be called only when serving the graph command."""
154 # -- Sanity checks.
155 assert self.apio_env.targeting_one_of("graph")
156 assert self.apio_env.params.target.HasField("graph")
158 # -- Shortcuts.
159 apio_env = self.apio_env
160 params = apio_env.params
161 graph_params = params.target.graph
163 # -- Determine the output type string.
164 type_map = {
165 GraphOutputType.PDF: "pdf",
166 GraphOutputType.PNG: "png",
167 GraphOutputType.SVG: "svg",
168 }
169 type_str = type_map[graph_params.output_type]
170 assert type_str, f"Unexpected graph type {graph_params.output_type}"
172 def completion_action(
173 target: list[File],
174 source: list[File],
175 env: SConsEnvironment,
176 ):
177 """Action function that prints a completion message and if
178 requested, open a viewer on the output file.."""
179 _ = (source, env) # Unused
180 # -- Get the rendered file.
181 target_file: File = target[0]
182 assert isinstance(target_file, File)
183 # -- Print a message
184 cout(f"Generated {str(target_file)}", style=SUCCESS)
185 # -- If requested, convert the file to URI and open it in the
186 # -- default browser.
187 if graph_params.open_viewer: 187 ↛ 188line 187 didn't jump to line 188 because the condition on line 187 was never true
188 cout("Opening default browser")
189 file_path = Path(target_file.get_abspath())
190 file_uri = file_path.resolve().as_uri()
191 default_browser = webbrowser.get()
192 default_browser.open(file_uri)
193 else:
194 cout("User requested no graph viewer")
196 actions = [
197 f"dot -T{type_str} $SOURCES -o $TARGET",
198 Action(completion_action, "completion_action"),
199 ]
201 graphviz_builder = cast(
202 BuilderBase,
203 Builder(
204 # Expecting graphviz dot to be installed and in the path.
205 action=actions,
206 src_suffix=".dot",
207 suffix=f".{type_str}",
208 ),
209 )
211 return graphviz_builder
213 def make_lint_config_builder(self) -> BuilderBase: # pragma: no cover
214 """Creates and returns the lint config builder."""
215 raise NotImplementedError("Implement in subclass.")
217 def make_lint_builder(self) -> BuilderBase: # pragma: no cover
218 """Creates and returns the lint builder."""
219 raise NotImplementedError("Implement in subclass.")