Coverage for apio / scons / plugin_base.py: 91%
65 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +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
16import webbrowser
17from SCons.Builder import BuilderBase
18from SCons.Action import Action
19from SCons.Script import Builder
20from SCons.Node.FS import File
21from SCons.Script.SConscript import SConsEnvironment
22from SCons.Node.Alias import Alias
23from apio.common.apio_console import cout
24from apio.common.apio_styles import SUCCESS
25from apio.common.common_util import SRC_SUFFIXES
26from apio.scons.apio_env import ApioEnv
27from apio.common.proto.apio_pb2 import GraphOutputType
28from apio.scons.plugin_util import (
29 verilog_src_scanner,
30 get_constraint_file,
31 get_define_flags,
32)
35# -- Supported apio graph types.
36SUPPORTED_GRAPH_TYPES = ["svg", "pdf", "png"]
39@dataclass(frozen=True)
40class ArchPluginInfo:
41 """Provides information about the plugin."""
43 # -- The suffix of the constraint file.
44 constrains_file_suffix: str
45 # -- The suffix of the nextpnr generated file.
46 pnr_file_suffix: str
47 # -- The suffix of the bitstream file.
48 bitstream_file_suffix: str
49 # -- Index of the significant term when parsing clock names.
50 clk_name_index: int
53class PluginBase:
54 """Base apio arch plugin handler"""
56 def __init__(self, apio_env: ApioEnv):
57 self.apio_env = apio_env
59 # -- Scanner for verilog source files.
60 self.verilog_src_scanner = verilog_src_scanner(apio_env)
62 # -- A placeholder for the constraint file name.
63 self._constrain_file: str = None
65 def plugin_info(self) -> ArchPluginInfo: # pragma: no cover
66 """Return plugin specific parameters."""
67 raise NotImplementedError("Implement in subclass.")
69 def constrain_file(self) -> str:
70 """Finds and returns the constraint file path."""
71 # -- Keep short references.
72 apio_env = self.apio_env
74 # -- On first call, determine and cache.
75 if self._constrain_file is None:
76 self._constrain_file = get_constraint_file(
77 apio_env, self.plugin_info().constrains_file_suffix
78 )
79 return self._constrain_file
81 def synth_builder(self) -> BuilderBase: # pragma: no cover
82 """Creates and returns the synth builder."""
83 raise NotImplementedError("Implement in subclass.")
85 def pnr_builder(self) -> BuilderBase: # pragma: no cover
86 """Creates and returns the pnr builder."""
87 raise NotImplementedError("Implement in subclass.")
89 def bitstream_builder(self) -> BuilderBase: # pragma: no cover
90 """Creates and returns the bitstream builder."""
91 raise NotImplementedError("Implement in subclass.")
93 def testbench_compile_builder(self) -> BuilderBase: # pragma: no cover
94 """Creates and returns the testbench compile builder."""
95 raise NotImplementedError("Implement in subclass.")
97 def testbench_run_builder(self) -> BuilderBase:
98 """Creates and returns the testbench run builder."""
100 # -- Sanity checks
101 assert self.apio_env.targeting_one_of("sim", "test")
102 assert self.apio_env.params.target.HasField(
103 "sim"
104 ) or self.apio_env.params.target.HasField("test")
106 return Builder(
107 action="vvp $SOURCE -dumpfile=$TARGET",
108 suffix=".vcd",
109 src_suffix=".out",
110 )
112 def yosys_dot_builder(self) -> BuilderBase:
113 """Creates and returns the yosys dot builder. Should be called
114 only when serving the graph command."""
116 # -- Sanity checks
117 assert self.apio_env.targeting_one_of("graph")
118 assert self.apio_env.params.target.HasField("graph")
120 # -- Shortcuts.
121 apio_env = self.apio_env
122 params = apio_env.params
123 graph_params = params.target.graph
125 # -- Determine top module value. First priority is to the
126 # -- graph cmd param.
127 top_module = (
128 graph_params.top_module
129 if graph_params.top_module
130 else params.apio_env_params.top_module
131 )
133 return Builder(
134 # See https://tinyurl.com/yosys-sv-graph
135 # For -wireshape see https://github.com/YosysHQ/yosys/pull/4252
136 action=(
137 'yosys -p "read_verilog -sv $SOURCES; show -format dot'
138 ' -colors 1 -wireshape plaintext -prefix {0} {1}" '
139 "-DSYNTHESIZE {2} {3}"
140 ).format(
141 apio_env.graph_target,
142 top_module,
143 "" if params.verbosity.all else "-q",
144 get_define_flags(apio_env),
145 ),
146 suffix=".dot",
147 src_suffix=SRC_SUFFIXES,
148 source_scanner=self.verilog_src_scanner,
149 )
151 def graphviz_renderer_builder(self) -> BuilderBase:
152 """Creates and returns the graphviz renderer builder. Should
153 be called only when serving the graph command."""
155 # -- Sanity checks.
156 assert self.apio_env.targeting_one_of("graph")
157 assert self.apio_env.params.target.HasField("graph")
159 # -- Shortcuts.
160 apio_env = self.apio_env
161 params = apio_env.params
162 graph_params = params.target.graph
164 # -- Determine the output type string.
165 type_map = {
166 GraphOutputType.PDF: "pdf",
167 GraphOutputType.PNG: "png",
168 GraphOutputType.SVG: "svg",
169 }
170 type_str = type_map[graph_params.output_type]
171 assert type_str, f"Unexpected graph type {graph_params.output_type}"
173 def completion_action(
174 target: List[Alias],
175 source: List[File],
176 env: SConsEnvironment,
177 ): # noqa
178 """Action function that prints a completion message and if
179 requested, open a viewer on the output file.."""
180 _ = (source, env) # Unused
181 # -- Get the rendered file.
182 target_file: File = target[0]
183 assert isinstance(target_file, File)
184 # -- Print a message
185 cout(f"Generated {str(target_file)}", style=SUCCESS)
186 # -- If requested, convert the file to URI and open it in the
187 # -- default browser.
188 if graph_params.open_viewer: 188 ↛ 189line 188 didn't jump to line 189 because the condition on line 188 was never true
189 cout("Opening default browser")
190 file_path = Path(target_file.get_abspath())
191 file_uri = file_path.resolve().as_uri()
192 default_browser = webbrowser.get()
193 default_browser.open(file_uri)
194 else:
195 cout("User requested no graph viewer")
197 actions = [
198 f"dot -T{type_str} $SOURCES -o $TARGET",
199 Action(completion_action, "completion_action"),
200 ]
202 graphviz_builder = Builder(
203 # Expecting graphviz dot to be installed and in the path.
204 action=actions,
205 suffix=f".{type_str}",
206 src_suffix=".dot",
207 )
209 return graphviz_builder
211 def lint_config_builder(self) -> BuilderBase: # pragma: no cover
212 """Creates and returns the lint config builder."""
213 raise NotImplementedError("Implement in subclass.")
215 def lint_builder(self) -> BuilderBase: # pragma: no cover
216 """Creates and returns the lint builder."""
217 raise NotImplementedError("Implement in subclass.")