Coverage for apio/scons/plugin_base.py: 91%

64 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-24 03:51 +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 

10 

11"""Apio scons related utilities..""" 

12 

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 

22 

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) 

34 

35 

36# -- Supported apio graph types. 

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

38 

39 

40@dataclass(frozen=True) 

41class ArchPluginInfo: 

42 """Provides information about the plugin.""" 

43 

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 

50 # -- Index of the significant term when parsing clock names. 

51 clk_name_index: int 

52 

53 

54class PluginBase: 

55 """Base apio arch plugin handler""" 

56 

57 def __init__(self, apio_env: ApioEnv): 

58 self.apio_env = apio_env 

59 

60 # -- Scanner for verilog source files. 

61 self.verilog_src_scanner = verilog_src_scanner(apio_env) 

62 

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

64 self._constrain_file: str | None = None 

65 

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

67 """Return plugin specific parameters.""" 

68 raise NotImplementedError("Implement in subclass.") 

69 

70 def constrain_file(self) -> str: 

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

72 # -- Keep short references. 

73 apio_env = self.apio_env 

74 

75 # -- On first call, determine and cache. 

76 if self._constrain_file is None: 

77 self._constrain_file = get_constraint_file( 

78 apio_env, self.plugin_info().constrains_file_suffix 

79 ) 

80 return self._constrain_file 

81 

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

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

84 raise NotImplementedError("Implement in subclass.") 

85 

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

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

88 raise NotImplementedError("Implement in subclass.") 

89 

90 def bitstream_builder(self) -> BuilderBase: # pragma: no cover 

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

92 raise NotImplementedError("Implement in subclass.") 

93 

94 def testbench_compile_builder(self) -> BuilderBase: # pragma: no cover 

95 """Creates and returns the testbench compile builder.""" 

96 raise NotImplementedError("Implement in subclass.") 

97 

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

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

100 

101 # -- Sanity checks 

102 assert self.apio_env.targeting_one_of("sim", "test") 

103 assert self.apio_env.params.target.HasField( 

104 "sim" 

105 ) or self.apio_env.params.target.HasField("test") 

106 

107 return Builder( 

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

109 suffix=".vcd", 

110 src_suffix=".out", 

111 ) 

112 

113 def yosys_dot_builder(self) -> BuilderBase | CompositeBuilder: 

114 """Creates and returns the yosys dot builder. Should be called 

115 only when serving the graph command.""" 

116 

117 # -- Sanity checks 

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

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

120 

121 # -- Shortcuts. 

122 apio_env = self.apio_env 

123 params = apio_env.params 

124 graph_params = params.target.graph 

125 

126 # -- Determine top module value. First priority is to the 

127 # -- graph cmd param. 

128 top_module = ( 

129 graph_params.top_module 

130 if graph_params.top_module 

131 else params.apio_env_params.top_module 

132 ) 

133 

134 return Builder( 

135 # See https://tinyurl.com/yosys-sv-graph 

136 # For -wireshape see https://github.com/YosysHQ/yosys/pull/4252 

137 action=( 

138 'yosys -p "read_verilog -sv $SOURCES; show -format dot' 

139 ' -colors 1 -wireshape plaintext -prefix {0} {1}" ' 

140 "-DSYNTHESIZE {2} {3}" 

141 ).format( 

142 apio_env.graph_target, 

143 top_module, 

144 "" if params.verbosity.all else "-q", 

145 get_define_flags(apio_env), 

146 ), 

147 suffix=".dot", 

148 src_suffix=SRC_SUFFIXES, 

149 source_scanner=self.verilog_src_scanner, 

150 ) 

151 

152 def graphviz_renderer_builder(self) -> BuilderBase: 

153 """Creates and returns the graphviz renderer builder. Should 

154 be called only when serving the graph command.""" 

155 

156 # -- Sanity checks. 

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

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

159 

160 # -- Shortcuts. 

161 apio_env = self.apio_env 

162 params = apio_env.params 

163 graph_params = params.target.graph 

164 

165 # -- Determine the output type string. 

166 type_map = { 

167 GraphOutputType.PDF: "pdf", 

168 GraphOutputType.PNG: "png", 

169 GraphOutputType.SVG: "svg", 

170 } 

171 type_str = type_map[graph_params.output_type] 

172 assert type_str, f"Unexpected graph type {graph_params.output_type}" 

173 

174 def completion_action( 

175 target: List[File], 

176 source: List[File], 

177 env: SConsEnvironment, 

178 ): # noqa 

179 """Action function that prints a completion message and if 

180 requested, open a viewer on the output file..""" 

181 _ = (source, env) # Unused 

182 # -- Get the rendered file. 

183 target_file: File = target[0] 

184 assert isinstance(target_file, File) 

185 # -- Print a message 

186 cout(f"Generated {str(target_file)}", style=SUCCESS) 

187 # -- If requested, convert the file to URI and open it in the 

188 # -- default browser. 

189 if graph_params.open_viewer: 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true

190 cout("Opening default browser") 

191 file_path = Path(target_file.get_abspath()) 

192 file_uri = file_path.resolve().as_uri() 

193 default_browser = webbrowser.get() 

194 default_browser.open(file_uri) 

195 else: 

196 cout("User requested no graph viewer") 

197 

198 actions = [ 

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

200 Action(completion_action, "completion_action"), 

201 ] 

202 

203 graphviz_builder = cast( 

204 BuilderBase, 

205 Builder( 

206 # Expecting graphviz dot to be installed and in the path. 

207 action=actions, 

208 suffix=f".{type_str}", 

209 src_suffix=".dot", 

210 ), 

211 ) 

212 

213 return graphviz_builder 

214 

215 def lint_config_builder(self) -> BuilderBase: # pragma: no cover 

216 """Creates and returns the lint config builder.""" 

217 raise NotImplementedError("Implement in subclass.") 

218 

219 def lint_builder(self) -> BuilderBase: # pragma: no cover 

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

221 raise NotImplementedError("Implement in subclass.")