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

64 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 01:55 +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 

51 

52class PluginBase: 

53 """Base apio arch plugin handler""" 

54 

55 def __init__(self, apio_env: ApioEnv): 

56 self.apio_env = apio_env 

57 

58 # -- Scanner for verilog source files. 

59 self.verilog_src_scanner = verilog_src_scanner(apio_env) 

60 

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

62 self._constrain_file: str | None = None 

63 

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

65 """Return plugin specific parameters.""" 

66 raise NotImplementedError("Implement in subclass.") 

67 

68 def constrain_file(self) -> str: 

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

70 # -- Keep short references. 

71 apio_env = self.apio_env 

72 

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

74 if self._constrain_file is None: 

75 self._constrain_file = get_constraint_file( 

76 apio_env, self.plugin_info().constrains_file_suffix 

77 ) 

78 return self._constrain_file 

79 

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

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

82 raise NotImplementedError("Implement in subclass.") 

83 

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

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

86 raise NotImplementedError("Implement in subclass.") 

87 

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

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

90 raise NotImplementedError("Implement in subclass.") 

91 

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

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

94 raise NotImplementedError("Implement in subclass.") 

95 

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

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

98 

99 # -- Sanity checks 

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

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

102 "sim" 

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

104 

105 return Builder( 

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

107 suffix=".vcd", 

108 src_suffix=".out", 

109 ) 

110 

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

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

113 only when serving the graph command.""" 

114 

115 # -- Sanity checks 

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

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

118 

119 # -- Shortcuts. 

120 apio_env = self.apio_env 

121 params = apio_env.params 

122 graph_params = params.target.graph 

123 

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

125 # -- graph cmd param. 

126 top_module = ( 

127 graph_params.top_module 

128 if graph_params.top_module 

129 else params.apio_env_params.top_module 

130 ) 

131 

132 return Builder( 

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

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

135 action=( 

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

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

138 "-DSYNTHESIZE {2} {3}" 

139 ).format( 

140 apio_env.graph_target, 

141 top_module, 

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

143 get_define_flags(apio_env), 

144 ), 

145 suffix=".dot", 

146 src_suffix=SRC_SUFFIXES, 

147 source_scanner=self.verilog_src_scanner, 

148 ) 

149 

150 def graphviz_renderer_builder(self) -> BuilderBase: 

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

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

153 

154 # -- Sanity checks. 

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

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

157 

158 # -- Shortcuts. 

159 apio_env = self.apio_env 

160 params = apio_env.params 

161 graph_params = params.target.graph 

162 

163 # -- Determine the output type string. 

164 type_map = { 

165 GraphOutputType.PDF: "pdf", 

166 GraphOutputType.PNG: "png", 

167 GraphOutputType.SVG: "svg", 

168 } 

169 type_str = type_map[graph_params.output_type] 

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

171 

172 def completion_action( 

173 target: List[File], 

174 source: List[File], 

175 env: SConsEnvironment, 

176 ): # noqa 

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

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

179 _ = (source, env) # Unused 

180 # -- Get the rendered file. 

181 target_file: File = target[0] 

182 assert isinstance(target_file, File) 

183 # -- Print a message 

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

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

186 # -- default browser. 

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

188 cout("Opening default browser") 

189 file_path = Path(target_file.get_abspath()) 

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

191 default_browser = webbrowser.get() 

192 default_browser.open(file_uri) 

193 else: 

194 cout("User requested no graph viewer") 

195 

196 actions = [ 

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

198 Action(completion_action, "completion_action"), 

199 ] 

200 

201 graphviz_builder = cast( 

202 BuilderBase, 

203 Builder( 

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

205 action=actions, 

206 suffix=f".{type_str}", 

207 src_suffix=".dot", 

208 ), 

209 ) 

210 

211 return graphviz_builder 

212 

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

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

215 raise NotImplementedError("Implement in subclass.") 

216 

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

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

219 raise NotImplementedError("Implement in subclass.")