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

65 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 02:47 +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_one_of("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_one_of("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}" ' 

134 "-DSYNTHESIZE {2} {3}" 

135 ).format( 

136 apio_env.graph_target, 

137 top_module, 

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

139 get_define_flags(apio_env), 

140 ), 

141 suffix=".dot", 

142 src_suffix=SRC_SUFFIXES, 

143 source_scanner=self.verilog_src_scanner, 

144 ) 

145 

146 def graphviz_renderer_builder(self) -> BuilderBase: 

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

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

149 

150 # -- Sanity checks. 

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

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

153 

154 # -- Shortcuts. 

155 apio_env = self.apio_env 

156 params = apio_env.params 

157 graph_params = params.target.graph 

158 

159 # -- Determine the output type string. 

160 type_map = { 

161 GraphOutputType.PDF: "pdf", 

162 GraphOutputType.PNG: "png", 

163 GraphOutputType.SVG: "svg", 

164 } 

165 type_str = type_map[graph_params.output_type] 

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

167 

168 def completion_action( 

169 target: List[Alias], 

170 source: List[File], 

171 env: SConsEnvironment, 

172 ): # noqa 

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

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

175 _ = (source, env) # Unused 

176 # -- Get the rendered file. 

177 target_file: File = target[0] 

178 assert isinstance(target_file, File) 

179 # -- Print a message 

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

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

182 # -- default browser. 

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

184 cout("Opening default browser") 

185 file_path = Path(target_file.get_abspath()) 

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

187 default_browser = webbrowser.get() 

188 default_browser.open(file_uri) 

189 else: 

190 cout("User requested no graph viewer") 

191 

192 actions = [ 

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

194 Action(completion_action, "completion_action"), 

195 ] 

196 

197 graphviz_builder = Builder( 

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

199 action=actions, 

200 suffix=f".{type_str}", 

201 src_suffix=".dot", 

202 ) 

203 

204 return graphviz_builder 

205 

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

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

208 raise NotImplementedError("Implement in subclass.") 

209 

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

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

212 raise NotImplementedError("Implement in subclass.")