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

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 

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) 

33 

34 

35# -- Supported apio graph types. 

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

37 

38 

39@dataclass(frozen=True) 

40class ArchPluginInfo: 

41 """Provides information about the plugin.""" 

42 

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 

51 

52 

53class PluginBase: 

54 """Base apio arch plugin handler""" 

55 

56 def __init__(self, apio_env: ApioEnv): 

57 self.apio_env = apio_env 

58 

59 # -- Scanner for verilog source files. 

60 self.verilog_src_scanner = verilog_src_scanner(apio_env) 

61 

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

63 self._constrain_file: str = None 

64 

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

66 """Return plugin specific parameters.""" 

67 raise NotImplementedError("Implement in subclass.") 

68 

69 def constrain_file(self) -> str: 

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

71 # -- Keep short references. 

72 apio_env = self.apio_env 

73 

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 

80 

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

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

83 raise NotImplementedError("Implement in subclass.") 

84 

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

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

87 raise NotImplementedError("Implement in subclass.") 

88 

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

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

91 raise NotImplementedError("Implement in subclass.") 

92 

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

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

95 raise NotImplementedError("Implement in subclass.") 

96 

97 def testbench_run_builder(self) -> BuilderBase: 

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

99 

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

105 

106 return Builder( 

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

108 suffix=".vcd", 

109 src_suffix=".out", 

110 ) 

111 

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

115 

116 # -- Sanity checks 

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

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

119 

120 # -- Shortcuts. 

121 apio_env = self.apio_env 

122 params = apio_env.params 

123 graph_params = params.target.graph 

124 

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 ) 

132 

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 ) 

150 

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

154 

155 # -- Sanity checks. 

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

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

158 

159 # -- Shortcuts. 

160 apio_env = self.apio_env 

161 params = apio_env.params 

162 graph_params = params.target.graph 

163 

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

172 

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

196 

197 actions = [ 

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

199 Action(completion_action, "completion_action"), 

200 ] 

201 

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 ) 

208 

209 return graphviz_builder 

210 

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

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

213 raise NotImplementedError("Implement in subclass.") 

214 

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

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

217 raise NotImplementedError("Implement in subclass.")