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

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 

8 

9"""Apio scons related utilities..""" 

10 

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 

20 

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) 

32 

33 

34# -- Supported apio graph types. 

35SUPPORTED_GRAPH_TYPES = ["svg", "pdf", "png"] 

36 

37 

38@dataclass(frozen=True) 

39class ArchPluginInfo: 

40 """Provides information about the plugin.""" 

41 

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 

48 

49 

50class PluginBase: 

51 """Base apio arch plugin handler""" 

52 

53 def __init__(self, apio_env: ApioEnv): 

54 self.apio_env = apio_env 

55 

56 # -- Scanner for verilog source files. 

57 self.verilog_src_scanner = verilog_src_scanner(apio_env) 

58 

59 # -- A placeholder for the constraint file name. 

60 self._constrain_file: str | None = None 

61 

62 def plugin_info(self) -> ArchPluginInfo: # pragma: no cover 

63 """Return plugin specific parameters.""" 

64 raise NotImplementedError("Implement in subclass.") 

65 

66 def constrain_file(self) -> str: 

67 """Finds and returns the constraint file path.""" 

68 # -- Keep short references. 

69 apio_env = self.apio_env 

70 

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 

77 

78 def make_synth_builder(self) -> BuilderBase: # pragma: no cover 

79 """Creates and returns the synth builder.""" 

80 raise NotImplementedError("Implement in subclass.") 

81 

82 def make_pnr_builder(self) -> BuilderBase: # pragma: no cover 

83 """Creates and returns the pnr builder.""" 

84 raise NotImplementedError("Implement in subclass.") 

85 

86 def make_bitstream_builder(self) -> BuilderBase: # pragma: no cover 

87 """Creates and returns the bitstream builder.""" 

88 raise NotImplementedError("Implement in subclass.") 

89 

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.") 

95 

96 def testbench_run_builder(self) -> BuilderBase | CompositeBuilder: 

97 """Creates and returns the testbench run builder.""" 

98 

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") 

104 

105 return Builder( 

106 action="vvp $SOURCE -dumpfile=$TARGET", 

107 src_suffix=".out", 

108 suffix=".vcd", 

109 ) 

110 

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.""" 

114 

115 # -- Sanity checks 

116 assert self.apio_env.targeting_one_of("graph") 

117 assert self.apio_env.params.target.HasField("graph") 

118 

119 # -- Shortcuts. 

120 apio_env = self.apio_env 

121 params = apio_env.params 

122 graph_params = params.target.graph 

123 

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 ) 

131 

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 ) 

149 

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.""" 

153 

154 # -- Sanity checks. 

155 assert self.apio_env.targeting_one_of("graph") 

156 assert self.apio_env.params.target.HasField("graph") 

157 

158 # -- Shortcuts. 

159 apio_env = self.apio_env 

160 params = apio_env.params 

161 graph_params = params.target.graph 

162 

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}" 

171 

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") 

195 

196 actions = [ 

197 f"dot -T{type_str} $SOURCES -o $TARGET", 

198 Action(completion_action, "completion_action"), 

199 ] 

200 

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 ) 

210 

211 return graphviz_builder 

212 

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.") 

216 

217 def make_lint_builder(self) -> BuilderBase: # pragma: no cover 

218 """Creates and returns the lint builder.""" 

219 raise NotImplementedError("Implement in subclass.")