Coverage for apio/scons/plugin_base.py: 91%
64 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +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
52class PluginBase:
53 """Base apio arch plugin handler"""
55 def __init__(self, apio_env: ApioEnv):
56 self.apio_env = apio_env
58 # -- Scanner for verilog source files.
59 self.verilog_src_scanner = verilog_src_scanner(apio_env)
61 # -- A placeholder for the constraint file name.
62 self._constrain_file: str | None = None
64 def plugin_info(self) -> ArchPluginInfo: # pragma: no cover
65 """Return plugin specific parameters."""
66 raise NotImplementedError("Implement in subclass.")
68 def constrain_file(self) -> str:
69 """Finds and returns the constraint file path."""
70 # -- Keep short references.
71 apio_env = self.apio_env
73 # -- On first call, determine and cache.
74 if self._constrain_file is None:
75 self._constrain_file = get_constraint_file(
76 apio_env, self.plugin_info().constrains_file_suffix
77 )
78 return self._constrain_file
80 def synth_builder(self) -> BuilderBase: # pragma: no cover
81 """Creates and returns the synth builder."""
82 raise NotImplementedError("Implement in subclass.")
84 def pnr_builder(self) -> BuilderBase: # pragma: no cover
85 """Creates and returns the pnr builder."""
86 raise NotImplementedError("Implement in subclass.")
88 def bitstream_builder(self) -> BuilderBase: # pragma: no cover
89 """Creates and returns the bitstream builder."""
90 raise NotImplementedError("Implement in subclass.")
92 def testbench_compile_builder(self) -> 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 suffix=".vcd",
108 src_suffix=".out",
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 suffix=".dot",
146 src_suffix=SRC_SUFFIXES,
147 source_scanner=self.verilog_src_scanner,
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 ): # noqa
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 suffix=f".{type_str}",
207 src_suffix=".dot",
208 ),
209 )
211 return graphviz_builder
213 def lint_config_builder(self) -> BuilderBase: # pragma: no cover
214 """Creates and returns the lint config builder."""
215 raise NotImplementedError("Implement in subclass.")
217 def lint_builder(self) -> BuilderBase: # pragma: no cover
218 """Creates and returns the lint builder."""
219 raise NotImplementedError("Implement in subclass.")