Coverage for apio / scons / plugin_base.py: 91%
65 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 02:47 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 02:47 +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 constrains_file_ext: str
44 bin_file_suffix: str
45 clk_name_index: int
48class PluginBase:
49 """Base apio arch plugin handler"""
51 def __init__(self, apio_env: ApioEnv):
52 self.apio_env = apio_env
54 # -- Scanner for verilog source files.
55 self.verilog_src_scanner = verilog_src_scanner(apio_env)
57 # -- A placeholder for the constraint file name.
58 self._constrain_file: str = None
60 def plugin_info(self) -> ArchPluginInfo: # pragma: no cover
61 """Return plugin specific parameters."""
62 raise NotImplementedError("Implement in subclass.")
64 def constrain_file(self) -> str:
65 """Finds and returns the constraint file path."""
66 # -- Keep short references.
67 apio_env = self.apio_env
69 # -- On first call, determine and cache.
70 if self._constrain_file is None:
71 self._constrain_file = get_constraint_file(
72 apio_env, self.plugin_info().constrains_file_ext
73 )
74 return self._constrain_file
76 def synth_builder(self) -> BuilderBase: # pragma: no cover
77 """Creates and returns the synth builder."""
78 raise NotImplementedError("Implement in subclass.")
80 def pnr_builder(self) -> BuilderBase: # pragma: no cover
81 """Creates and returns the pnr builder."""
82 raise NotImplementedError("Implement in subclass.")
84 def bitstream_builder(self) -> BuilderBase: # pragma: no cover
85 """Creates and returns the bitstream builder."""
86 raise NotImplementedError("Implement in subclass.")
88 def testbench_compile_builder(self) -> BuilderBase: # pragma: no cover
89 """Creates and returns the testbench compile builder."""
90 raise NotImplementedError("Implement in subclass.")
92 def testbench_run_builder(self) -> BuilderBase:
93 """Creates and returns the testbench run builder."""
95 # -- Sanity checks
96 assert self.apio_env.targeting_one_of("sim", "test")
97 assert self.apio_env.params.target.HasField(
98 "sim"
99 ) or self.apio_env.params.target.HasField("test")
101 return Builder(
102 action="vvp $SOURCE -dumpfile=$TARGET",
103 suffix=".vcd",
104 src_suffix=".out",
105 )
107 def yosys_dot_builder(self) -> BuilderBase:
108 """Creates and returns the yosys dot builder. Should be called
109 only when serving the graph command."""
111 # -- Sanity checks
112 assert self.apio_env.targeting_one_of("graph")
113 assert self.apio_env.params.target.HasField("graph")
115 # -- Shortcuts.
116 apio_env = self.apio_env
117 params = apio_env.params
118 graph_params = params.target.graph
120 # -- Determine top module value. First priority is to the
121 # -- graph cmd param.
122 top_module = (
123 graph_params.top_module
124 if graph_params.top_module
125 else params.apio_env_params.top_module
126 )
128 return Builder(
129 # See https://tinyurl.com/yosys-sv-graph
130 # For -wireshape see https://github.com/YosysHQ/yosys/pull/4252
131 action=(
132 'yosys -p "read_verilog -sv $SOURCES; show -format dot'
133 ' -colors 1 -wireshape plaintext -prefix {0} {1}" '
134 "-DSYNTHESIZE {2} {3}"
135 ).format(
136 apio_env.graph_target,
137 top_module,
138 "" if params.verbosity.all else "-q",
139 get_define_flags(apio_env),
140 ),
141 suffix=".dot",
142 src_suffix=SRC_SUFFIXES,
143 source_scanner=self.verilog_src_scanner,
144 )
146 def graphviz_renderer_builder(self) -> BuilderBase:
147 """Creates and returns the graphviz renderer builder. Should
148 be called only when serving the graph command."""
150 # -- Sanity checks.
151 assert self.apio_env.targeting_one_of("graph")
152 assert self.apio_env.params.target.HasField("graph")
154 # -- Shortcuts.
155 apio_env = self.apio_env
156 params = apio_env.params
157 graph_params = params.target.graph
159 # -- Determine the output type string.
160 type_map = {
161 GraphOutputType.PDF: "pdf",
162 GraphOutputType.PNG: "png",
163 GraphOutputType.SVG: "svg",
164 }
165 type_str = type_map[graph_params.output_type]
166 assert type_str, f"Unexpected graph type {graph_params.output_type}"
168 def completion_action(
169 target: List[Alias],
170 source: List[File],
171 env: SConsEnvironment,
172 ): # noqa
173 """Action function that prints a completion message and if
174 requested, open a viewer on the output file.."""
175 _ = (source, env) # Unused
176 # -- Get the rendered file.
177 target_file: File = target[0]
178 assert isinstance(target_file, File)
179 # -- Print a message
180 cout(f"Generated {str(target_file)}", style=SUCCESS)
181 # -- If requested, convert the file to URI and open it in the
182 # -- default browser.
183 if graph_params.open_viewer: 183 ↛ 184line 183 didn't jump to line 184 because the condition on line 183 was never true
184 cout("Opening default browser")
185 file_path = Path(target_file.get_abspath())
186 file_uri = file_path.resolve().as_uri()
187 default_browser = webbrowser.get()
188 default_browser.open(file_uri)
189 else:
190 cout("User requested no graph viewer")
192 actions = [
193 f"dot -T{type_str} $SOURCES -o $TARGET",
194 Action(completion_action, "completion_action"),
195 ]
197 graphviz_builder = Builder(
198 # Expecting graphviz dot to be installed and in the path.
199 action=actions,
200 suffix=f".{type_str}",
201 src_suffix=".dot",
202 )
204 return graphviz_builder
206 def lint_config_builder(self) -> BuilderBase: # pragma: no cover
207 """Creates and returns the lint config builder."""
208 raise NotImplementedError("Implement in subclass.")
210 def lint_builder(self) -> BuilderBase: # pragma: no cover
211 """Creates and returns the lint builder."""
212 raise NotImplementedError("Implement in subclass.")