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

63 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +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 

15import webbrowser 

16from SCons.Builder import BuilderBase 

17from SCons.Action import Action 

18from SCons.Script import Builder 

19from SCons.Node.FS import File 

20from apio.common.apio_console import cout 

21from apio.common.apio_styles import SUCCESS 

22from apio.common.common_util import SRC_SUFFIXES 

23from apio.scons.apio_env import ApioEnv 

24from apio.common.proto.apio_pb2 import GraphOutputType 

25from apio.scons.plugin_util import ( 

26 verilog_src_scanner, 

27 get_constraint_file, 

28 get_define_flags, 

29) 

30 

31 

32# -- Supported apio graph types. 

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

34 

35 

36@dataclass(frozen=True) 

37class ArchPluginInfo: 

38 """Provides information about the plugin.""" 

39 

40 constrains_file_ext: str 

41 bin_file_suffix: str 

42 clk_name_index: int 

43 

44 

45class PluginBase: 

46 """Base apio arch plugin handler""" 

47 

48 def __init__(self, apio_env: ApioEnv): 

49 self.apio_env = apio_env 

50 

51 # -- Scanner for verilog source files. 

52 self.verilog_src_scanner = verilog_src_scanner(apio_env) 

53 

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

55 self._constrain_file: str = None 

56 

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

58 """Return plugin specific parameters.""" 

59 raise NotImplementedError("Implement in subclass.") 

60 

61 def constrain_file(self) -> str: 

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

63 # -- Keep short references. 

64 apio_env = self.apio_env 

65 

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

67 if self._constrain_file is None: 

68 self._constrain_file = get_constraint_file( 

69 apio_env, self.plugin_info().constrains_file_ext 

70 ) 

71 return self._constrain_file 

72 

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

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

75 raise NotImplementedError("Implement in subclass.") 

76 

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

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

79 raise NotImplementedError("Implement in subclass.") 

80 

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

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

83 raise NotImplementedError("Implement in subclass.") 

84 

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

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

87 raise NotImplementedError("Implement in subclass.") 

88 

89 def testbench_run_builder(self) -> BuilderBase: 

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

91 

92 # -- Sanity checks 

93 assert self.apio_env.targeting("sim", "test") 

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

95 "sim" 

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

97 

98 return Builder( 

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

100 suffix=".vcd", 

101 src_suffix=".out", 

102 ) 

103 

104 def yosys_dot_builder(self) -> BuilderBase: 

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

106 only when serving the graph command.""" 

107 

108 # -- Sanity checks 

109 assert self.apio_env.targeting("graph") 

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

111 

112 # -- Shortcuts. 

113 apio_env = self.apio_env 

114 params = apio_env.params 

115 graph_params = params.target.graph 

116 

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

118 # -- graph cmd param. 

119 top_module = ( 

120 graph_params.top_module 

121 if graph_params.top_module 

122 else params.apio_env_params.top_module 

123 ) 

124 

125 return Builder( 

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

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

128 action=( 

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

130 ' -colors 1 -wireshape plaintext -prefix {0} {1}" {2} {3}' 

131 ).format( 

132 apio_env.graph_target, 

133 top_module, 

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

135 get_define_flags(apio_env), 

136 ), 

137 suffix=".dot", 

138 src_suffix=SRC_SUFFIXES, 

139 source_scanner=self.verilog_src_scanner, 

140 ) 

141 

142 def graphviz_renderer_builder(self) -> BuilderBase: 

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

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

145 

146 # -- Sanity checks. 

147 assert self.apio_env.targeting("graph") 

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

149 

150 # -- Shortcuts. 

151 apio_env = self.apio_env 

152 params = apio_env.params 

153 graph_params = params.target.graph 

154 

155 # -- Determine the output type string. 

156 type_map = { 

157 GraphOutputType.PDF: "pdf", 

158 GraphOutputType.PNG: "png", 

159 GraphOutputType.SVG: "svg", 

160 } 

161 type_str = type_map[graph_params.output_type] 

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

163 

164 def completion_action(source, target, env): # noqa 

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

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

167 _ = source # Unused 

168 _ = env # Unused 

169 # -- Get the rendered file. 

170 target_file: File = target[0] 

171 assert isinstance(target_file, File) 

172 # -- Print a message 

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

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

175 # -- default browser. 

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

177 cout("Opening default browser") 

178 file_path = Path(target_file.get_abspath()) 

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

180 default_browser = webbrowser.get() 

181 default_browser.open(file_uri) 

182 else: 

183 cout("User requested no graph viewer") 

184 

185 actions = [ 

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

187 Action(completion_action, "completion_action"), 

188 ] 

189 

190 graphviz_builder = Builder( 

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

192 action=actions, 

193 suffix=f".{type_str}", 

194 src_suffix=".dot", 

195 ) 

196 

197 return graphviz_builder 

198 

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

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

201 raise NotImplementedError("Implement in subclass.") 

202 

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

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

205 raise NotImplementedError("Implement in subclass.")