build.plat: don't check for toolchain presence if do_build=False.
[nmigen.git] / nmigen / build / plat.py
1 from collections import OrderedDict
2 from abc import ABCMeta, abstractmethod, abstractproperty
3 import os
4 import textwrap
5 import re
6 import jinja2
7
8 from .. import __version__
9 from .._toolchain import *
10 from ..hdl import *
11 from ..hdl.xfrm import SampleLowerer, DomainLowerer
12 from ..lib.cdc import ResetSynchronizer
13 from ..back import rtlil, verilog
14 from .res import *
15 from .run import *
16
17
18 __all__ = ["Platform", "TemplatedPlatform"]
19
20
21 class Platform(ResourceManager, metaclass=ABCMeta):
22 resources = abstractproperty()
23 connectors = abstractproperty()
24 default_clk = None
25 default_rst = None
26 required_tools = abstractproperty()
27
28 def __init__(self):
29 super().__init__(self.resources, self.connectors)
30
31 self.extra_files = OrderedDict()
32
33 self._prepared = False
34
35 @property
36 def default_clk_constraint(self):
37 if self.default_clk is None:
38 raise AttributeError("Platform '{}' does not define a default clock"
39 .format(type(self).__name__))
40 return self.lookup(self.default_clk).clock
41
42 @property
43 def default_clk_frequency(self):
44 constraint = self.default_clk_constraint
45 if constraint is None:
46 raise AttributeError("Platform '{}' does not constrain its default clock"
47 .format(type(self).__name__))
48 return constraint.frequency
49
50 def add_file(self, filename, content):
51 if not isinstance(filename, str):
52 raise TypeError("File name must be a string, not {!r}"
53 .format(filename))
54 if hasattr(content, "read"):
55 content = content.read()
56 elif not isinstance(content, (str, bytes)):
57 raise TypeError("File contents must be str, bytes, or a file-like object, not {!r}"
58 .format(content))
59 if filename in self.extra_files:
60 if self.extra_files[filename] != content:
61 raise ValueError("File {!r} already exists"
62 .format(filename))
63 else:
64 self.extra_files[filename] = content
65
66 @property
67 def _toolchain_env_var(self):
68 return f"NMIGEN_ENV_{self.toolchain}"
69
70 def build(self, elaboratable, name="top",
71 build_dir="build", do_build=True,
72 program_opts=None, do_program=False,
73 **kwargs):
74 # The following code performs a best-effort check for presence of required tools upfront,
75 # before performing any build actions, to provide a better diagnostic. It does not handle
76 # several corner cases:
77 # 1. `require_tool` does not source toolchain environment scripts, so if such a script
78 # is used, the check is skipped, and `execute_local()` may fail;
79 # 2. if the design is not built (do_build=False), most of the tools are not required and
80 # in fact might not be available if the design will be built manually with a different
81 # environment script specified, or on a different machine; however, Yosys is required
82 # by virtually every platform anyway, to provide debug Verilog output, and `prepare()`
83 # may fail.
84 # This is OK because even if `require_tool` succeeds, the toolchain might be broken anyway.
85 # The check only serves to catch common errors earlier.
86 if do_build and self._toolchain_env_var not in os.environ:
87 for tool in self.required_tools:
88 require_tool(tool)
89
90 plan = self.prepare(elaboratable, name, **kwargs)
91 if not do_build:
92 return plan
93
94 products = plan.execute_local(build_dir)
95 if not do_program:
96 return products
97
98 self.toolchain_program(products, name, **(program_opts or {}))
99
100 def has_required_tools(self):
101 if self._toolchain_env_var in os.environ:
102 return True
103 return all(has_tool(name) for name in self.required_tools)
104
105 def create_missing_domain(self, name):
106 # Simple instantiation of a clock domain driven directly by the board clock and reset.
107 # This implementation uses a single ResetSynchronizer to ensure that:
108 # * an external reset is definitely synchronized to the system clock;
109 # * release of power-on reset, which is inherently asynchronous, is synchronized to
110 # the system clock.
111 # Many device families provide advanced primitives for tackling reset. If these exist,
112 # they should be used instead.
113 if name == "sync" and self.default_clk is not None:
114 clk_i = self.request(self.default_clk).i
115 if self.default_rst is not None:
116 rst_i = self.request(self.default_rst).i
117 else:
118 rst_i = Const(0)
119
120 m = Module()
121 m.domains += ClockDomain("sync")
122 m.d.comb += ClockSignal("sync").eq(clk_i)
123 m.submodules.reset_sync = ResetSynchronizer(rst_i, domain="sync")
124 return m
125
126 def prepare(self, elaboratable, name="top", **kwargs):
127 assert not self._prepared
128 self._prepared = True
129
130 fragment = Fragment.get(elaboratable, self)
131 fragment = SampleLowerer()(fragment)
132 fragment._propagate_domains(self.create_missing_domain, platform=self)
133 fragment = DomainLowerer()(fragment)
134
135 def add_pin_fragment(pin, pin_fragment):
136 pin_fragment = Fragment.get(pin_fragment, self)
137 if not isinstance(pin_fragment, Instance):
138 pin_fragment.flatten = True
139 fragment.add_subfragment(pin_fragment, name="pin_{}".format(pin.name))
140
141 for pin, port, attrs, invert in self.iter_single_ended_pins():
142 if pin.dir == "i":
143 add_pin_fragment(pin, self.get_input(pin, port, attrs, invert))
144 if pin.dir == "o":
145 add_pin_fragment(pin, self.get_output(pin, port, attrs, invert))
146 if pin.dir == "oe":
147 add_pin_fragment(pin, self.get_tristate(pin, port, attrs, invert))
148 if pin.dir == "io":
149 add_pin_fragment(pin, self.get_input_output(pin, port, attrs, invert))
150
151 for pin, p_port, n_port, attrs, invert in self.iter_differential_pins():
152 if pin.dir == "i":
153 add_pin_fragment(pin, self.get_diff_input(pin, p_port, n_port, attrs, invert))
154 if pin.dir == "o":
155 add_pin_fragment(pin, self.get_diff_output(pin, p_port, n_port, attrs, invert))
156 if pin.dir == "oe":
157 add_pin_fragment(pin, self.get_diff_tristate(pin, p_port, n_port, attrs, invert))
158 if pin.dir == "io":
159 add_pin_fragment(pin,
160 self.get_diff_input_output(pin, p_port, n_port, attrs, invert))
161
162 fragment._propagate_ports(ports=self.iter_ports(), all_undef_as_ports=False)
163 return self.toolchain_prepare(fragment, name, **kwargs)
164
165 @abstractmethod
166 def toolchain_prepare(self, fragment, name, **kwargs):
167 """
168 Convert the ``fragment`` and constraints recorded in this :class:`Platform` into
169 a :class:`BuildPlan`.
170 """
171 raise NotImplementedError # :nocov:
172
173 def toolchain_program(self, products, name, **kwargs):
174 """
175 Extract bitstream for fragment ``name`` from ``products`` and download it to a target.
176 """
177 raise NotImplementedError("Platform '{}' does not support programming"
178 .format(type(self).__name__))
179
180 def _check_feature(self, feature, pin, attrs, valid_xdrs, valid_attrs):
181 if not valid_xdrs:
182 raise NotImplementedError("Platform '{}' does not support {}"
183 .format(type(self).__name__, feature))
184 elif pin.xdr not in valid_xdrs:
185 raise NotImplementedError("Platform '{}' does not support {} for XDR {}"
186 .format(type(self).__name__, feature, pin.xdr))
187
188 if not valid_attrs and attrs:
189 raise NotImplementedError("Platform '{}' does not support attributes for {}"
190 .format(type(self).__name__, feature))
191
192 @staticmethod
193 def _invert_if(invert, value):
194 if invert:
195 return ~value
196 else:
197 return value
198
199 def get_input(self, pin, port, attrs, invert):
200 self._check_feature("single-ended input", pin, attrs,
201 valid_xdrs=(0,), valid_attrs=None)
202
203 m = Module()
204 m.d.comb += pin.i.eq(self._invert_if(invert, port))
205 return m
206
207 def get_output(self, pin, port, attrs, invert):
208 self._check_feature("single-ended output", pin, attrs,
209 valid_xdrs=(0,), valid_attrs=None)
210
211 m = Module()
212 m.d.comb += port.eq(self._invert_if(invert, pin.o))
213 return m
214
215 def get_tristate(self, pin, port, attrs, invert):
216 self._check_feature("single-ended tristate", pin, attrs,
217 valid_xdrs=(0,), valid_attrs=None)
218
219 m = Module()
220 m.submodules += Instance("$tribuf",
221 p_WIDTH=pin.width,
222 i_EN=pin.oe,
223 i_A=self._invert_if(invert, pin.o),
224 o_Y=port,
225 )
226 return m
227
228 def get_input_output(self, pin, port, attrs, invert):
229 self._check_feature("single-ended input/output", pin, attrs,
230 valid_xdrs=(0,), valid_attrs=None)
231
232 m = Module()
233 m.submodules += Instance("$tribuf",
234 p_WIDTH=pin.width,
235 i_EN=pin.oe,
236 i_A=self._invert_if(invert, pin.o),
237 o_Y=port,
238 )
239 m.d.comb += pin.i.eq(self._invert_if(invert, port))
240 return m
241
242 def get_diff_input(self, pin, p_port, n_port, attrs, invert):
243 self._check_feature("differential input", pin, attrs,
244 valid_xdrs=(), valid_attrs=None)
245
246 def get_diff_output(self, pin, p_port, n_port, attrs, invert):
247 self._check_feature("differential output", pin, attrs,
248 valid_xdrs=(), valid_attrs=None)
249
250 def get_diff_tristate(self, pin, p_port, n_port, attrs, invert):
251 self._check_feature("differential tristate", pin, attrs,
252 valid_xdrs=(), valid_attrs=None)
253
254 def get_diff_input_output(self, pin, p_port, n_port, attrs, invert):
255 self._check_feature("differential input/output", pin, attrs,
256 valid_xdrs=(), valid_attrs=None)
257
258
259 class TemplatedPlatform(Platform):
260 toolchain = abstractproperty()
261 file_templates = abstractproperty()
262 command_templates = abstractproperty()
263
264 build_script_templates = {
265 "build_{{name}}.sh": """
266 # {{autogenerated}}
267 set -e{{verbose("x")}}
268 [ -n "${{platform._toolchain_env_var}}" ] && . "${{platform._toolchain_env_var}}"
269 {{emit_commands("sh")}}
270 """,
271 "build_{{name}}.bat": """
272 @rem {{autogenerated}}
273 {{quiet("@echo off")}}
274 if defined {{platform._toolchain_env_var}} call %{{platform._toolchain_env_var}}%
275 {{emit_commands("bat")}}
276 """,
277 }
278
279 def toolchain_prepare(self, fragment, name, **kwargs):
280 # Restrict the name of the design to a strict alphanumeric character set. Platforms will
281 # interpolate the name of the design in many different contexts: filesystem paths, Python
282 # scripts, Tcl scripts, ad-hoc constraint files, and so on. It is not practical to add
283 # escaping code that handles every one of their edge cases, so make sure we never hit them
284 # in the first place.
285 invalid_char = re.match(r"[^A-Za-z0-9_]", name)
286 if invalid_char:
287 raise ValueError("Design name {!r} contains invalid character {!r}; only alphanumeric "
288 "characters are valid in design names"
289 .format(name, invalid_char.group(0)))
290
291 # This notice serves a dual purpose: to explain that the file is autogenerated,
292 # and to incorporate the nMigen version into generated code.
293 autogenerated = "Automatically generated by nMigen {}. Do not edit.".format(__version__)
294
295 rtlil_text, name_map = rtlil.convert_fragment(fragment, name=name)
296
297 def emit_rtlil():
298 return rtlil_text
299
300 def emit_verilog(opts=()):
301 return verilog._convert_rtlil_text(rtlil_text,
302 strip_internal_attrs=True, write_verilog_opts=opts)
303
304 def emit_debug_verilog(opts=()):
305 return verilog._convert_rtlil_text(rtlil_text,
306 strip_internal_attrs=False, write_verilog_opts=opts)
307
308 def emit_commands(syntax):
309 commands = []
310
311 for name in self.required_tools:
312 env_var = tool_env_var(name)
313 if syntax == "sh":
314 template = ": ${{{env_var}:={name}}}"
315 elif syntax == "bat":
316 template = \
317 "if [%{env_var}%] equ [\"\"] set {env_var}=\n" \
318 "if [%{env_var}%] equ [] set {env_var}={name}"
319 else:
320 assert False
321 commands.append(template.format(env_var=env_var, name=name))
322
323 for index, command_tpl in enumerate(self.command_templates):
324 command = render(command_tpl, origin="<command#{}>".format(index + 1),
325 syntax=syntax)
326 command = re.sub(r"\s+", " ", command)
327 if syntax == "sh":
328 commands.append(command)
329 elif syntax == "bat":
330 commands.append(command + " || exit /b")
331 else:
332 assert False
333
334 return "\n".join(commands)
335
336 def get_override(var):
337 var_env = "NMIGEN_{}".format(var)
338 if var_env in os.environ:
339 # On Windows, there is no way to define an "empty but set" variable; it is tempting
340 # to use a quoted empty string, but it doesn't do what one would expect. Recognize
341 # this as a useful pattern anyway, and treat `set VAR=""` on Windows the same way
342 # `export VAR=` is treated on Linux.
343 return re.sub(r'^\"\"$', "", os.environ[var_env])
344 elif var in kwargs:
345 if isinstance(kwargs[var], str):
346 return textwrap.dedent(kwargs[var]).strip()
347 else:
348 return kwargs[var]
349 else:
350 return jinja2.Undefined(name=var)
351
352 @jinja2.contextfunction
353 def invoke_tool(context, name):
354 env_var = tool_env_var(name)
355 if context.parent["syntax"] == "sh":
356 return "\"${}\"".format(env_var)
357 elif context.parent["syntax"] == "bat":
358 return "%{}%".format(env_var)
359 else:
360 assert False
361
362 def options(opts):
363 if isinstance(opts, str):
364 return opts
365 else:
366 return " ".join(opts)
367
368 def hierarchy(signal, separator):
369 return separator.join(name_map[signal][1:])
370
371 def verbose(arg):
372 if "NMIGEN_verbose" in os.environ:
373 return arg
374 else:
375 return jinja2.Undefined(name="quiet")
376
377 def quiet(arg):
378 if "NMIGEN_verbose" in os.environ:
379 return jinja2.Undefined(name="quiet")
380 else:
381 return arg
382
383 def render(source, origin, syntax=None):
384 try:
385 source = textwrap.dedent(source).strip()
386 compiled = jinja2.Template(source, trim_blocks=True, lstrip_blocks=True)
387 compiled.environment.filters["options"] = options
388 compiled.environment.filters["hierarchy"] = hierarchy
389 except jinja2.TemplateSyntaxError as e:
390 e.args = ("{} (at {}:{})".format(e.message, origin, e.lineno),)
391 raise
392 return compiled.render({
393 "name": name,
394 "platform": self,
395 "emit_rtlil": emit_rtlil,
396 "emit_verilog": emit_verilog,
397 "emit_debug_verilog": emit_debug_verilog,
398 "emit_commands": emit_commands,
399 "syntax": syntax,
400 "invoke_tool": invoke_tool,
401 "get_override": get_override,
402 "verbose": verbose,
403 "quiet": quiet,
404 "autogenerated": autogenerated,
405 })
406
407 plan = BuildPlan(script="build_{}".format(name))
408 for filename_tpl, content_tpl in self.file_templates.items():
409 plan.add_file(render(filename_tpl, origin=filename_tpl),
410 render(content_tpl, origin=content_tpl))
411 for filename, content in self.extra_files.items():
412 plan.add_file(filename, content)
413 return plan
414
415 def iter_extra_files(self, *endswith):
416 return (f for f in self.extra_files if f.endswith(endswith))