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

65 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01:53 +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 constrains_file_ext: str 

44 bin_file_suffix: str 

45 clk_name_index: int 

46 

47 

48class PluginBase: 

49 """Base apio arch plugin handler""" 

50 

51 def __init__(self, apio_env: ApioEnv): 

52 self.apio_env = apio_env 

53 

54 # -- Scanner for verilog source files. 

55 self.verilog_src_scanner = verilog_src_scanner(apio_env) 

56 

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

58 self._constrain_file: str = None 

59 

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

61 """Return plugin specific parameters.""" 

62 raise NotImplementedError("Implement in subclass.") 

63 

64 def constrain_file(self) -> str: 

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

66 # -- Keep short references. 

67 apio_env = self.apio_env 

68 

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 

75 

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

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

78 raise NotImplementedError("Implement in subclass.") 

79 

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

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

82 raise NotImplementedError("Implement in subclass.") 

83 

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

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

86 raise NotImplementedError("Implement in subclass.") 

87 

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

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

90 raise NotImplementedError("Implement in subclass.") 

91 

92 def testbench_run_builder(self) -> BuilderBase: 

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

94 

95 # -- Sanity checks 

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

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

98 "sim" 

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

100 

101 return Builder( 

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

103 suffix=".vcd", 

104 src_suffix=".out", 

105 ) 

106 

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

110 

111 # -- Sanity checks 

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

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

114 

115 # -- Shortcuts. 

116 apio_env = self.apio_env 

117 params = apio_env.params 

118 graph_params = params.target.graph 

119 

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 ) 

127 

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}" {2} {3}' 

134 ).format( 

135 apio_env.graph_target, 

136 top_module, 

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

138 get_define_flags(apio_env), 

139 ), 

140 suffix=".dot", 

141 src_suffix=SRC_SUFFIXES, 

142 source_scanner=self.verilog_src_scanner, 

143 ) 

144 

145 def graphviz_renderer_builder(self) -> BuilderBase: 

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

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

148 

149 # -- Sanity checks. 

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

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

152 

153 # -- Shortcuts. 

154 apio_env = self.apio_env 

155 params = apio_env.params 

156 graph_params = params.target.graph 

157 

158 # -- Determine the output type string. 

159 type_map = { 

160 GraphOutputType.PDF: "pdf", 

161 GraphOutputType.PNG: "png", 

162 GraphOutputType.SVG: "svg", 

163 } 

164 type_str = type_map[graph_params.output_type] 

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

166 

167 def completion_action( 

168 target: List[Alias], 

169 source: List[File], 

170 env: SConsEnvironment, 

171 ): # noqa 

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

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

174 _ = (source, env) # Unused 

175 # -- Get the rendered file. 

176 target_file: File = target[0] 

177 assert isinstance(target_file, File) 

178 # -- Print a message 

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

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

181 # -- default browser. 

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

183 cout("Opening default browser") 

184 file_path = Path(target_file.get_abspath()) 

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

186 default_browser = webbrowser.get() 

187 default_browser.open(file_uri) 

188 else: 

189 cout("User requested no graph viewer") 

190 

191 actions = [ 

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

193 Action(completion_action, "completion_action"), 

194 ] 

195 

196 graphviz_builder = Builder( 

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

198 action=actions, 

199 suffix=f".{type_str}", 

200 src_suffix=".dot", 

201 ) 

202 

203 return graphviz_builder 

204 

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

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

207 raise NotImplementedError("Implement in subclass.") 

208 

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

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

211 raise NotImplementedError("Implement in subclass.")