siliconcompiler 0.32.3__py3-none-any.whl → 0.33.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. siliconcompiler/__init__.py +19 -2
  2. siliconcompiler/_metadata.py +1 -1
  3. siliconcompiler/apps/sc.py +2 -2
  4. siliconcompiler/apps/sc_install.py +3 -3
  5. siliconcompiler/apps/sc_issue.py +1 -1
  6. siliconcompiler/apps/sc_remote.py +4 -4
  7. siliconcompiler/apps/sc_show.py +2 -2
  8. siliconcompiler/apps/utils/replay.py +5 -3
  9. siliconcompiler/asic.py +120 -0
  10. siliconcompiler/checklist.py +150 -0
  11. siliconcompiler/core.py +267 -289
  12. siliconcompiler/flowgraph.py +803 -515
  13. siliconcompiler/fpga.py +84 -0
  14. siliconcompiler/metric.py +420 -0
  15. siliconcompiler/optimizer/vizier.py +2 -3
  16. siliconcompiler/package/__init__.py +29 -6
  17. siliconcompiler/pdk.py +415 -0
  18. siliconcompiler/record.py +449 -0
  19. siliconcompiler/remote/client.py +6 -3
  20. siliconcompiler/remote/schema.py +116 -112
  21. siliconcompiler/remote/server.py +3 -5
  22. siliconcompiler/report/dashboard/cli/__init__.py +13 -722
  23. siliconcompiler/report/dashboard/cli/board.py +895 -0
  24. siliconcompiler/report/dashboard/web/__init__.py +10 -10
  25. siliconcompiler/report/dashboard/web/components/__init__.py +5 -4
  26. siliconcompiler/report/dashboard/web/components/flowgraph.py +3 -3
  27. siliconcompiler/report/dashboard/web/components/graph.py +6 -3
  28. siliconcompiler/report/dashboard/web/state.py +1 -1
  29. siliconcompiler/report/dashboard/web/utils/__init__.py +4 -3
  30. siliconcompiler/report/html_report.py +2 -3
  31. siliconcompiler/report/report.py +13 -7
  32. siliconcompiler/report/summary_image.py +1 -1
  33. siliconcompiler/report/summary_table.py +3 -3
  34. siliconcompiler/report/utils.py +11 -10
  35. siliconcompiler/scheduler/__init__.py +145 -280
  36. siliconcompiler/scheduler/run_node.py +2 -1
  37. siliconcompiler/scheduler/send_messages.py +4 -4
  38. siliconcompiler/scheduler/slurm.py +2 -2
  39. siliconcompiler/schema/__init__.py +19 -2
  40. siliconcompiler/schema/baseschema.py +493 -0
  41. siliconcompiler/schema/cmdlineschema.py +250 -0
  42. siliconcompiler/{sphinx_ext → schema/docs}/__init__.py +3 -1
  43. siliconcompiler/{sphinx_ext → schema/docs}/dynamicgen.py +63 -81
  44. siliconcompiler/{sphinx_ext → schema/docs}/schemagen.py +73 -85
  45. siliconcompiler/{sphinx_ext → schema/docs}/utils.py +12 -13
  46. siliconcompiler/schema/editableschema.py +136 -0
  47. siliconcompiler/schema/journalingschema.py +238 -0
  48. siliconcompiler/schema/namedschema.py +41 -0
  49. siliconcompiler/schema/packageschema.py +101 -0
  50. siliconcompiler/schema/parameter.py +791 -0
  51. siliconcompiler/schema/parametertype.py +323 -0
  52. siliconcompiler/schema/parametervalue.py +736 -0
  53. siliconcompiler/schema/safeschema.py +37 -0
  54. siliconcompiler/schema/schema_cfg.py +109 -1789
  55. siliconcompiler/schema/utils.py +5 -68
  56. siliconcompiler/schema_obj.py +119 -0
  57. siliconcompiler/tool.py +1308 -0
  58. siliconcompiler/tools/_common/__init__.py +6 -10
  59. siliconcompiler/tools/_common/sdc/sc_constraints.sdc +1 -1
  60. siliconcompiler/tools/bluespec/convert.py +7 -7
  61. siliconcompiler/tools/builtin/_common.py +1 -1
  62. siliconcompiler/tools/builtin/concatenate.py +2 -2
  63. siliconcompiler/tools/builtin/minimum.py +1 -1
  64. siliconcompiler/tools/builtin/mux.py +2 -1
  65. siliconcompiler/tools/builtin/nop.py +1 -1
  66. siliconcompiler/tools/builtin/verify.py +6 -4
  67. siliconcompiler/tools/chisel/convert.py +4 -4
  68. siliconcompiler/tools/genfasm/bitstream.py +3 -3
  69. siliconcompiler/tools/ghdl/convert.py +1 -1
  70. siliconcompiler/tools/icarus/compile.py +4 -4
  71. siliconcompiler/tools/icepack/bitstream.py +6 -1
  72. siliconcompiler/tools/klayout/convert_drc_db.py +5 -0
  73. siliconcompiler/tools/klayout/klayout_export.py +0 -1
  74. siliconcompiler/tools/klayout/klayout_utils.py +3 -10
  75. siliconcompiler/tools/nextpnr/apr.py +6 -1
  76. siliconcompiler/tools/nextpnr/nextpnr.py +4 -4
  77. siliconcompiler/tools/openroad/_apr.py +13 -0
  78. siliconcompiler/tools/openroad/rdlroute.py +3 -3
  79. siliconcompiler/tools/openroad/scripts/apr/postamble.tcl +1 -1
  80. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +5 -5
  81. siliconcompiler/tools/openroad/scripts/apr/sc_antenna_repair.tcl +2 -2
  82. siliconcompiler/tools/openroad/scripts/apr/sc_clock_tree_synthesis.tcl +2 -2
  83. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_placement.tcl +2 -2
  84. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +2 -2
  85. siliconcompiler/tools/openroad/scripts/apr/sc_endcap_tapcell_insertion.tcl +2 -2
  86. siliconcompiler/tools/openroad/scripts/apr/sc_fillercell_insertion.tcl +2 -2
  87. siliconcompiler/tools/openroad/scripts/apr/sc_fillmetal_insertion.tcl +2 -2
  88. siliconcompiler/tools/openroad/scripts/apr/sc_global_placement.tcl +2 -2
  89. siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +2 -2
  90. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +2 -2
  91. siliconcompiler/tools/openroad/scripts/apr/sc_macro_placement.tcl +3 -3
  92. siliconcompiler/tools/openroad/scripts/apr/sc_metrics.tcl +2 -2
  93. siliconcompiler/tools/openroad/scripts/apr/sc_pin_placement.tcl +2 -2
  94. siliconcompiler/tools/openroad/scripts/apr/sc_power_grid.tcl +2 -2
  95. siliconcompiler/tools/openroad/scripts/apr/sc_repair_design.tcl +2 -2
  96. siliconcompiler/tools/openroad/scripts/apr/sc_repair_timing.tcl +2 -2
  97. siliconcompiler/tools/openroad/scripts/apr/sc_write_data.tcl +2 -2
  98. siliconcompiler/tools/openroad/scripts/common/procs.tcl +57 -1
  99. siliconcompiler/tools/openroad/scripts/common/screenshot.tcl +2 -2
  100. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +28 -3
  101. siliconcompiler/tools/openroad/scripts/sc_rcx.tcl +1 -1
  102. siliconcompiler/tools/openroad/scripts/sc_rdlroute.tcl +3 -3
  103. siliconcompiler/tools/openroad/scripts/sc_show.tcl +6 -6
  104. siliconcompiler/tools/slang/__init__.py +10 -10
  105. siliconcompiler/tools/surelog/parse.py +4 -4
  106. siliconcompiler/tools/sv2v/convert.py +20 -3
  107. siliconcompiler/tools/verilator/compile.py +2 -2
  108. siliconcompiler/tools/verilator/verilator.py +3 -3
  109. siliconcompiler/tools/vpr/place.py +1 -1
  110. siliconcompiler/tools/vpr/route.py +4 -4
  111. siliconcompiler/tools/vpr/screenshot.py +1 -1
  112. siliconcompiler/tools/vpr/show.py +5 -5
  113. siliconcompiler/tools/vpr/vpr.py +24 -24
  114. siliconcompiler/tools/xdm/convert.py +2 -2
  115. siliconcompiler/tools/xyce/simulate.py +1 -1
  116. siliconcompiler/tools/yosys/sc_synth_asic.tcl +74 -68
  117. siliconcompiler/tools/yosys/syn_asic.py +2 -2
  118. siliconcompiler/toolscripts/_tools.json +7 -7
  119. siliconcompiler/toolscripts/ubuntu22/install-vpr.sh +0 -2
  120. siliconcompiler/toolscripts/ubuntu24/install-vpr.sh +0 -2
  121. siliconcompiler/utils/__init__.py +8 -112
  122. siliconcompiler/utils/flowgraph.py +339 -0
  123. siliconcompiler/{issue.py → utils/issue.py} +4 -3
  124. siliconcompiler/utils/logging.py +1 -2
  125. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/METADATA +9 -8
  126. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/RECORD +151 -134
  127. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/WHEEL +1 -1
  128. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/entry_points.txt +8 -8
  129. siliconcompiler/schema/schema_obj.py +0 -1936
  130. siliconcompiler/toolscripts/ubuntu20/install-vpr.sh +0 -29
  131. siliconcompiler/toolscripts/ubuntu20/install-yosys-parmys.sh +0 -61
  132. /siliconcompiler/{templates → data/templates}/__init__.py +0 -0
  133. /siliconcompiler/{templates → data/templates}/email/__init__.py +0 -0
  134. /siliconcompiler/{templates → data/templates}/email/general.j2 +0 -0
  135. /siliconcompiler/{templates → data/templates}/email/summary.j2 +0 -0
  136. /siliconcompiler/{templates → data/templates}/issue/README.txt +0 -0
  137. /siliconcompiler/{templates → data/templates}/issue/__init__.py +0 -0
  138. /siliconcompiler/{templates → data/templates}/issue/run.sh +0 -0
  139. /siliconcompiler/{templates → data/templates}/replay/replay.py.j2 +0 -0
  140. /siliconcompiler/{templates → data/templates}/replay/replay.sh.j2 +0 -0
  141. /siliconcompiler/{templates → data/templates}/replay/requirements.txt +0 -0
  142. /siliconcompiler/{templates → data/templates}/replay/setup.sh +0 -0
  143. /siliconcompiler/{templates → data/templates}/report/__init__.py +0 -0
  144. /siliconcompiler/{templates → data/templates}/report/bootstrap.min.css +0 -0
  145. /siliconcompiler/{templates → data/templates}/report/bootstrap.min.js +0 -0
  146. /siliconcompiler/{templates → data/templates}/report/bootstrap_LICENSE.md +0 -0
  147. /siliconcompiler/{templates → data/templates}/report/sc_report.j2 +0 -0
  148. /siliconcompiler/{templates → data/templates}/slurm/__init__.py +0 -0
  149. /siliconcompiler/{templates → data/templates}/slurm/run.sh +0 -0
  150. /siliconcompiler/{templates → data/templates}/tcl/__init__.py +0 -0
  151. /siliconcompiler/{templates → data/templates}/tcl/manifest.tcl.j2 +0 -0
  152. /siliconcompiler/{units.py → utils/units.py} +0 -0
  153. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/licenses/LICENSE +0 -0
  154. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1308 @@
1
+ import contextlib
2
+ import os
3
+ import psutil
4
+ import re
5
+ import shlex
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import time
10
+
11
+ try:
12
+ import resource
13
+ except ModuleNotFoundError:
14
+ resource = None
15
+
16
+ try:
17
+ # Note: this import throws exception on Windows
18
+ import pty
19
+ except ModuleNotFoundError:
20
+ pty = None
21
+
22
+ import os.path
23
+
24
+ from packaging.version import Version, InvalidVersion
25
+ from packaging.specifiers import SpecifierSet, InvalidSpecifier
26
+
27
+ from siliconcompiler.schema import NamedSchema
28
+ from siliconcompiler.schema import EditableSchema, Parameter, PerNode, Scope
29
+ from siliconcompiler.schema.utils import trim
30
+
31
+ from siliconcompiler import utils
32
+ from siliconcompiler import sc_open
33
+
34
+ from siliconcompiler.record import RecordTool
35
+
36
+ from siliconcompiler.scheduler import print_traceback
37
+
38
+
39
+ class TaskError(Exception):
40
+ '''
41
+ Error indicates execution cannot continue and should be terminated
42
+ '''
43
+
44
+
45
+ class TaskTimeout(TaskError):
46
+ '''
47
+ Error indicates a timeout has occurred
48
+
49
+ Args:
50
+ timeout (float): execution time at timeout
51
+ '''
52
+ def __init__(self, *args, timeout=None, **kwargs):
53
+ super().__init__(*args, **kwargs)
54
+ self.timeout = timeout
55
+
56
+
57
+ class TaskExecutableNotFound(TaskError):
58
+ '''
59
+ Executable not found.
60
+ '''
61
+
62
+
63
+ class TaskSchema(NamedSchema):
64
+ def __init__(self, name=None):
65
+ super().__init__(name=name)
66
+
67
+ schema_task(self)
68
+
69
+
70
+ class ToolSchema(NamedSchema):
71
+ __parse_version_check_str = r"""
72
+ (?P<operator>(==|!=|<=|>=|<|>|~=))
73
+ \s*
74
+ (?P<version>
75
+ [^,;\s)]* # Since this is a "legacy" specifier, and the version
76
+ # string can be just about anything, we match everything
77
+ # except for whitespace, a semi-colon for marker support,
78
+ # a closing paren since versions can be enclosed in
79
+ # them, and a comma since it's a version separator.
80
+ )
81
+ """
82
+ __parse_version_check = re.compile(
83
+ r"^\s*" + __parse_version_check_str + r"\s*$",
84
+ re.VERBOSE | re.IGNORECASE)
85
+
86
+ def __init__(self, name=None):
87
+ super().__init__(name=name)
88
+
89
+ schema_tool(self)
90
+
91
+ schema = EditableSchema(self)
92
+ schema.insert("task", "default", TaskSchema())
93
+
94
+ self.set_runtime(None)
95
+
96
+ def set_runtime(self, chip):
97
+ '''
98
+ Sets the runtime information needed to properly execute a task.
99
+ Note: unstable API
100
+
101
+ Args:
102
+ chip (:class:`Chip`): root schema for the runtime information
103
+ '''
104
+ self.__chip = None
105
+ self.__schema_full = None
106
+ self.__logger = None
107
+ if chip:
108
+ self.__chip = chip
109
+ self.__schema_full = chip.schema
110
+ self.__logger = chip.logger
111
+
112
+ self.__step = None
113
+ self.__index = None
114
+ self.__tool = None
115
+ self.__task = None
116
+
117
+ self.__schema_record = None
118
+ self.__schema_metric = None
119
+ if self.__schema_full:
120
+ self.__schema_record = self.__schema_full.get("record", field="schema")
121
+ self.__schema_metric = self.__schema_full.get("metric", field="schema")
122
+
123
+ self.__step = self.__schema_full.get('arg', 'step')
124
+ self.__index = self.__schema_full.get('arg', 'index')
125
+
126
+ if not self.__step or not self.__index:
127
+ raise RuntimeError("step or index not specified")
128
+
129
+ flow = self.__schema_full.get('option', 'flow')
130
+ if not flow:
131
+ raise RuntimeError("flow not specified")
132
+ self.__tool = self.__schema_full.get(
133
+ 'flowgraph', flow, self.__step, self.__index, 'tool')
134
+ self.__task = self.__schema_full.get(
135
+ 'flowgraph', flow, self.__step, self.__index, 'task')
136
+
137
+ def node(self):
138
+ '''
139
+ Returns:
140
+ step and index for the current runtime
141
+ '''
142
+
143
+ return self.__step, self.__index
144
+
145
+ def task(self):
146
+ '''
147
+ Returns:
148
+ task name
149
+ '''
150
+
151
+ return self.__task
152
+
153
+ def logger(self):
154
+ '''
155
+ Returns:
156
+ logger
157
+ '''
158
+ return self.__logger
159
+
160
+ def schema(self, type=None):
161
+ '''
162
+ Get useful section of the schema.
163
+
164
+ Args:
165
+ type (str): schema section to find, if None returns the root schema.
166
+
167
+ Returns:
168
+ schema section.
169
+ '''
170
+ if type is None:
171
+ return self.__schema_full
172
+ elif type == "record":
173
+ return self.__schema_record
174
+ elif type == "metric":
175
+ return self.__schema_metric
176
+ else:
177
+ raise ValueError(f"{type} is not a schema section")
178
+
179
+ def get_exe(self):
180
+ '''
181
+ Determines the absolute path for the specified executable.
182
+
183
+ Raises:
184
+ :class:`TaskExecutableNotFound`: if executable not found.
185
+
186
+ Returns:
187
+ path to executable, or None if not specified
188
+ '''
189
+
190
+ exe = self.get('exe')
191
+
192
+ if exe is None:
193
+ return None
194
+
195
+ # Collect path
196
+ env = self.get_runtime_environmental_variables(include_path=True)
197
+
198
+ fullexe = shutil.which(exe, path=env["PATH"])
199
+
200
+ if not fullexe:
201
+ raise TaskExecutableNotFound(f"{exe} could not be found")
202
+
203
+ return fullexe
204
+
205
+ def get_exe_version(self):
206
+ '''
207
+ Gets the version of the specified executable.
208
+
209
+ Raises:
210
+ :class:`TaskExecutableNotFound`: if executable not found.
211
+ :class:`NotImplementedError`: if :meth:`.parse_version` has not be implemented.
212
+
213
+ Returns:
214
+ version determined by :meth:`.parse_version`.
215
+ '''
216
+
217
+ veropt = self.get('vswitch')
218
+ if not veropt:
219
+ return None
220
+
221
+ exe = self.get_exe()
222
+ if not exe:
223
+ return None
224
+
225
+ exe_path, exe_base = os.path.split(exe)
226
+
227
+ cmdlist = [exe]
228
+ cmdlist.extend(veropt)
229
+
230
+ self.__logger.debug(f'Running {self.name()} version check: {" ".join(cmdlist)}')
231
+
232
+ proc = subprocess.run(cmdlist,
233
+ stdin=subprocess.DEVNULL,
234
+ stdout=subprocess.PIPE,
235
+ stderr=subprocess.STDOUT,
236
+ universal_newlines=True)
237
+
238
+ if proc.returncode != 0:
239
+ self.__logger.warning(f"Version check on '{exe_base}' ended with "
240
+ f"code {proc.returncode}")
241
+
242
+ try:
243
+ version = self.parse_version(proc.stdout)
244
+ except NotImplementedError:
245
+ raise NotImplementedError(f'{self.name()} does not implement parse_version()')
246
+ except Exception as e:
247
+ self.__logger.error(f'{self.name()} failed to parse version string: {proc.stdout}')
248
+ raise e from None
249
+
250
+ self.__logger.info(f"Tool '{exe_base}' found with version '{version}' "
251
+ f"in directory '{exe_path}'")
252
+
253
+ return version
254
+
255
+ def check_exe_version(self, reported_version):
256
+ '''
257
+ Check if the reported version matches the versions specified in
258
+ :keypath:`tool,<tool>,version`.
259
+
260
+ Args:
261
+ reported_version (str): version to check
262
+
263
+ Returns:
264
+ True if the version matched, false otherwise
265
+
266
+ '''
267
+
268
+ spec_sets = self.get('version', step=self.__step, index=self.__index)
269
+ if not spec_sets:
270
+ # No requirement so always true
271
+ return True
272
+
273
+ for spec_set in spec_sets:
274
+ split_specs = [s.strip() for s in spec_set.split(",") if s.strip()]
275
+ specs_list = []
276
+ for spec in split_specs:
277
+ match = re.match(ToolSchema.__parse_version_check, spec)
278
+ if match is None:
279
+ self.__logger.warning(f'Invalid version specifier {spec}. '
280
+ f'Defaulting to =={spec}.')
281
+ operator = '=='
282
+ spec_version = spec
283
+ else:
284
+ operator = match.group('operator')
285
+ spec_version = match.group('version')
286
+ specs_list.append((operator, spec_version))
287
+
288
+ try:
289
+ normalized_version = self.normalize_version(reported_version)
290
+ except Exception as e:
291
+ self.__logger.error(f'Unable to normalize version for {self.name()}: '
292
+ f'{reported_version}')
293
+ raise e from None
294
+
295
+ try:
296
+ version = Version(normalized_version)
297
+ except InvalidVersion:
298
+ self.__logger.error(f'Version {normalized_version} reported by {self.name()} does '
299
+ 'not match standard.')
300
+ return False
301
+
302
+ try:
303
+ normalized_spec_list = [
304
+ f'{op}{self.normalize_version(ver)}' for op, ver in specs_list]
305
+ normalized_specs = ','.join(normalized_spec_list)
306
+ except Exception as e:
307
+ self.__logger.error(f'Unable to normalize versions for {self.name()}: '
308
+ f'{",".join([f"{op}{ver}" for op, ver in specs_list])}')
309
+ raise e from None
310
+
311
+ try:
312
+ spec_set = SpecifierSet(normalized_specs)
313
+ except InvalidSpecifier:
314
+ self.__logger.error(f'Version specifier set {normalized_specs} '
315
+ 'does not match standard.')
316
+ return False
317
+
318
+ if version in spec_set:
319
+ return True
320
+
321
+ allowedstr = '; '.join(spec_sets)
322
+ self.__logger.error(f"Version check failed for {self.name()}. Check installation.")
323
+ self.__logger.error(f"Found version {reported_version}, "
324
+ f"did not satisfy any version specifier set {allowedstr}.")
325
+ return False
326
+
327
+ def get_runtime_environmental_variables(self, include_path=True):
328
+ '''
329
+ Determine the environmental variables needed for the task
330
+
331
+ Args:
332
+ include_path (bool): if True, includes PATH variable
333
+
334
+ Returns:
335
+ dict of str: dictionary of environmental variable to value mapping
336
+ '''
337
+
338
+ # Add global environmental vars
339
+ envvars = {}
340
+ for env in self.__schema_full.getkeys('option', 'env'):
341
+ envvars[env] = self.__schema_full.get('option', 'env', env)
342
+
343
+ # Add tool specific vars
344
+ for lic_env in self.getkeys('licenseserver'):
345
+ license_file = self.get('licenseserver', lic_env, step=self.__step, index=self.__index)
346
+ if license_file:
347
+ envvars[lic_env] = ':'.join(license_file)
348
+
349
+ if include_path:
350
+ path_param = self.get('path', field=None, step=self.__step, index=self.__index)
351
+ if path_param.get(field='package'):
352
+ raise NotImplementedError
353
+
354
+ envvars["PATH"] = os.getenv("PATH", os.defpath)
355
+
356
+ path = path_param.get(field=None).resolve_path() # TODO: needs package search
357
+ if path:
358
+ envvars["PATH"] = path + os.pathsep + envvars["PATH"]
359
+
360
+ # Forward additional variables
361
+ for var in ('LD_LIBRARY_PATH',):
362
+ val = os.getenv(var, None)
363
+ if val:
364
+ envvars[var] = val
365
+
366
+ # Add task specific vars
367
+ for env in self.getkeys('task', self.__task, 'env'):
368
+ envvars[env] = self.get('task', self.__task, 'env', env,
369
+ step=self.__step, index=self.__index)
370
+
371
+ return envvars
372
+
373
+ def get_runtime_arguments(self):
374
+ '''
375
+ Constructs the arguments needed to run the task.
376
+
377
+ Returns:
378
+ command (list)
379
+ '''
380
+
381
+ cmdargs = []
382
+ cmdargs.extend(self.get('task', self.__task, 'option',
383
+ step=self.__step, index=self.__index))
384
+
385
+ # Add scripts files / TODO:
386
+ scripts = self.__chip.find_files('tool', self.__tool, 'task', self.__task, 'script',
387
+ step=self.__step, index=self.__index)
388
+
389
+ cmdargs.extend(scripts)
390
+
391
+ try:
392
+ cmdargs.extend(self.runtime_options())
393
+ except Exception as e:
394
+ self.__logger.error(f'Failed to get runtime options for {self.name()}/{self.__task}')
395
+ raise e from None
396
+
397
+ # Cleanup args
398
+ cmdargs = [str(arg).strip() for arg in cmdargs]
399
+
400
+ return cmdargs
401
+
402
+ def generate_replay_script(self, filepath, workdir, include_path=True):
403
+ '''
404
+ Generate a replay script for the task.
405
+
406
+ Args:
407
+ filepath (path): path to the file to write
408
+ workdir (path): path to the run work directory
409
+ include_path (bool): include path information in environmental variables
410
+ '''
411
+ replay_opts = {}
412
+ replay_opts["work_dir"] = workdir
413
+ replay_opts["exports"] = self.get_runtime_environmental_variables(include_path=include_path)
414
+
415
+ replay_opts["executable"] = self.get('exe')
416
+
417
+ vswitch = self.get('vswitch')
418
+ if vswitch:
419
+ replay_opts["version_flag"] = shlex.join(vswitch)
420
+
421
+ # detect arguments
422
+ arg_test = re.compile(r'^[-+]')
423
+
424
+ # detect file paths
425
+ file_test = re.compile(r'^[/\.]')
426
+
427
+ format_cmd = [replay_opts["executable"]]
428
+
429
+ for cmdarg in self.get_runtime_arguments():
430
+ add_new_line = len(format_cmd) == 1
431
+
432
+ if arg_test.match(cmdarg) or file_test.match(cmdarg):
433
+ add_new_line = True
434
+ else:
435
+ if not arg_test.match(format_cmd[-1]):
436
+ add_new_line = True
437
+
438
+ cmdarg = shlex.quote(cmdarg)
439
+ if add_new_line:
440
+ format_cmd.append(cmdarg)
441
+ else:
442
+ format_cmd[-1] += f' {cmdarg}'
443
+
444
+ replay_opts["cmds"] = format_cmd
445
+
446
+ # create replay file
447
+ with open(filepath, 'w') as f:
448
+ f.write(utils.get_file_template("replay/replay.sh.j2").render(replay_opts))
449
+ f.write("\n")
450
+
451
+ os.chmod(filepath, 0o755)
452
+
453
+ def setup_work_directory(self, workdir, remove_exist=True):
454
+ '''
455
+ Create the runtime directories needed to execute a task.
456
+
457
+ Args:
458
+ workdir (path): path to the run work directory
459
+ remove_exist (bool): if True, removes the existing directory
460
+ '''
461
+
462
+ # Delete existing directory
463
+ if os.path.isdir(workdir) and remove_exist:
464
+ shutil.rmtree(workdir)
465
+
466
+ # Create directories
467
+ os.makedirs(workdir, exist_ok=True)
468
+ os.makedirs(os.path.join(workdir, 'inputs'), exist_ok=True)
469
+ os.makedirs(os.path.join(workdir, 'outputs'), exist_ok=True)
470
+ os.makedirs(os.path.join(workdir, 'reports'), exist_ok=True)
471
+
472
+ def write_task_manifest(self, directory, backup=True):
473
+ '''
474
+ Write the manifest needed for the task
475
+
476
+ Args:
477
+ directory (path): directory to write the manifest into.
478
+ backup (bool): if True and an existing manifest is found a backup is kept.
479
+ '''
480
+
481
+ suffix = self.get('format')
482
+ if not suffix:
483
+ return
484
+
485
+ manifest_path = os.path.join(directory, f"sc_manifest.{suffix}")
486
+
487
+ if backup and os.path.exists(manifest_path):
488
+ shutil.copyfile(manifest_path, f'{manifest_path}.bak')
489
+
490
+ # TODO: pull in TCL/yaml here
491
+ self.__chip.write_manifest(manifest_path, abspath=True)
492
+
493
+ def __get_io_file(self, io_type):
494
+ '''
495
+ Get the runtime destination for the io type.
496
+
497
+ Args:
498
+ io_type (str): name of io type
499
+ '''
500
+ suffix = self.get('task', self.__task, io_type, 'suffix',
501
+ step=self.__step, index=self.__index)
502
+ destination = self.get('task', self.__task, io_type, 'destination',
503
+ step=self.__step, index=self.__index)
504
+
505
+ io_file = None
506
+ io_log = False
507
+ if destination == 'log':
508
+ io_file = f"{self.__step}.{suffix}"
509
+ io_log = True
510
+ elif destination == 'output':
511
+ io_file = os.path.join('outputs', f"{self.__chip.top()}.{suffix}")
512
+ elif destination == 'none':
513
+ io_file = os.devnull
514
+
515
+ return io_file, io_log
516
+
517
+ def __terminate_exe(self, proc):
518
+ '''
519
+ Terminates a subprocess
520
+
521
+ Args:
522
+ proc (subprocess.Process): process to terminate
523
+ '''
524
+
525
+ def terminate_process(pid, timeout=3):
526
+ '''Terminates a process and all its (grand+)children.
527
+
528
+ Based on https://psutil.readthedocs.io/en/latest/#psutil.wait_procs and
529
+ https://psutil.readthedocs.io/en/latest/#kill-process-tree.
530
+ '''
531
+ parent = psutil.Process(pid)
532
+ children = parent.children(recursive=True)
533
+ children.append(parent)
534
+ for p in children:
535
+ try:
536
+ p.terminate()
537
+ except psutil.NoSuchProcess:
538
+ # Process may have terminated on its own in the meantime
539
+ pass
540
+
541
+ _, alive = psutil.wait_procs(children, timeout=timeout)
542
+ for p in alive:
543
+ # If processes are still alive after timeout seconds, send more
544
+ # aggressive signal.
545
+ p.kill()
546
+
547
+ TERMINATE_TIMEOUT = 5
548
+
549
+ terminate_process(proc.pid, timeout=TERMINATE_TIMEOUT)
550
+ self.__logger.info(f'Waiting for {self.name()} to exit...')
551
+ try:
552
+ proc.wait(timeout=TERMINATE_TIMEOUT)
553
+ except subprocess.TimeoutExpired:
554
+ if proc.poll() is None:
555
+ self.__logger.warning(f'{self.name()} did not exit within {TERMINATE_TIMEOUT} '
556
+ 'seconds. Terminating...')
557
+ terminate_process(proc.pid, timeout=TERMINATE_TIMEOUT)
558
+
559
+ def run_task(self, workdir, quiet, loglevel, breakpoint, nice, timeout):
560
+ '''
561
+ Run the task.
562
+
563
+ Raises:
564
+ :class:`TaskError`: raised if the task failed to complete and
565
+ should not be considered complete.
566
+ :class:`TaskTimeout`: raised if the task reaches a timeout
567
+
568
+ Args:
569
+ workdir (path): path to the run work directory
570
+ quiet (bool): if True, execution output is suppressed
571
+ loglevel (str): logging level
572
+ breakpoint (bool): if True, will attempt to execute with a breakpoint
573
+ nice (int): POSIX nice level to use in execution
574
+ timeout (int): timeout to use for execution
575
+
576
+ Returns:
577
+ return code from the execution
578
+ '''
579
+
580
+ # TODO: Currently no memory usage tracking in breakpoints, builtins, or unexpected errors.
581
+ max_mem_bytes = 0
582
+ cpu_start = time.time()
583
+
584
+ # Ensure directories are setup
585
+ self.setup_work_directory(workdir, remove_exist=False)
586
+
587
+ # Write task manifest
588
+ self.write_task_manifest(workdir)
589
+
590
+ # Get file IO
591
+ stdout_file, is_stdout_log = self.__get_io_file("stdout")
592
+ stderr_file, is_stderr_log = self.__get_io_file("stderr")
593
+
594
+ stdout_print = self.__logger.info
595
+ stderr_print = self.__logger.error
596
+ if loglevel == "quiet":
597
+ stdout_print = self.__logger.error
598
+ stderr_print = self.__logger.error
599
+
600
+ def read_stdio(stdout_reader, stderr_reader):
601
+ if quiet:
602
+ return
603
+
604
+ if is_stdout_log and stdout_reader:
605
+ for line in stdout_reader.readlines():
606
+ stdout_print(line.rstrip())
607
+ if is_stderr_log and stderr_reader:
608
+ for line in stderr_reader.readlines():
609
+ stderr_print(line.rstrip())
610
+
611
+ exe = self.get_exe()
612
+
613
+ retcode = 0
614
+ if not exe:
615
+ # No executable, so must call run()
616
+ try:
617
+ with open(stdout_file, 'w') as stdout_writer, \
618
+ open(stderr_file, 'w') as stderr_writer:
619
+ if stderr_file == stdout_file:
620
+ stderr_writer.close()
621
+ stderr_writer = sys.stdout
622
+
623
+ with contextlib.redirect_stderr(stderr_writer), \
624
+ contextlib.redirect_stdout(stdout_writer):
625
+ retcode = self.run()
626
+ except Exception as e:
627
+ self.__logger.error(f'Failed in run() for {self.name()}/{self.__task}: {e}')
628
+ retcode = 1 # default to non-zero
629
+ print_traceback(self.__chip, e)
630
+ raise e
631
+ finally:
632
+ with sc_open(stdout_file) as stdout_reader, \
633
+ sc_open(stderr_file) as stderr_reader:
634
+ read_stdio(stdout_reader, stderr_reader)
635
+
636
+ try:
637
+ if resource:
638
+ # Since memory collection is not possible, collect the current process
639
+ # peak memory
640
+ max_mem_bytes = max(
641
+ max_mem_bytes,
642
+ 1024 * resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
643
+ except (OSError, ValueError, PermissionError):
644
+ pass
645
+ else:
646
+ cmdlist = self.get_runtime_arguments()
647
+
648
+ # Make record of tool options
649
+ self.schema("record").record_tool(
650
+ self.__step, self.__index,
651
+ cmdlist, RecordTool.ARGS)
652
+
653
+ self.__logger.info(shlex.join([os.path.basename(exe), *cmdlist]))
654
+
655
+ if not pty and breakpoint:
656
+ # pty not available
657
+ breakpoint = False
658
+
659
+ if breakpoint and sys.platform in ('darwin', 'linux'):
660
+ # When we break on a step, the tool often drops into a shell.
661
+ # However, our usual subprocess scheme seems to break terminal
662
+ # echo for some tools. On POSIX-compatible systems, we can use
663
+ # pty to connect the tool to our terminal instead. This code
664
+ # doesn't handle quiet/timeout logic, since we don't want either
665
+ # of these features for an interactive session. Logic for
666
+ # forwarding to file based on
667
+ # https://docs.python.org/3/library/pty.html#example.
668
+ with open(f"{self.__step}.log", 'wb') as log_writer:
669
+ def read(fd):
670
+ data = os.read(fd, 1024)
671
+ log_writer.write(data)
672
+ return data
673
+ retcode = pty.spawn(cmdlist, read)
674
+ else:
675
+ with open(stdout_file, 'w') as stdout_writer, \
676
+ open(stdout_file, 'r', errors='replace_with_warning') as stdout_reader, \
677
+ open(stderr_file, 'w') as stderr_writer, \
678
+ open(stderr_file, 'r', errors='replace_with_warning') as stderr_reader:
679
+ # if STDOUT and STDERR are to be redirected to the same file,
680
+ # use a single writer
681
+ if stderr_file == stdout_file:
682
+ stderr_writer.close()
683
+ stderr_reader.close()
684
+ stderr_reader = None
685
+ stderr_writer = subprocess.STDOUT
686
+
687
+ preexec_fn = None
688
+ if nice is not None and hasattr(os, 'nice'):
689
+ def set_task_nice():
690
+ os.nice(nice)
691
+ preexec_fn = set_task_nice
692
+
693
+ try:
694
+ proc = subprocess.Popen([exe, *cmdlist],
695
+ stdin=subprocess.DEVNULL,
696
+ stdout=stdout_writer,
697
+ stderr=stderr_writer,
698
+ preexec_fn=preexec_fn)
699
+ except Exception as e:
700
+ raise TaskError(f"Unable to start {exe}: {str(e)}")
701
+
702
+ # How long to wait for proc to quit on ctrl-c before force
703
+ # terminating.
704
+ POLL_INTERVAL = 0.1
705
+ MEMORY_WARN_LIMIT = 90
706
+ try:
707
+ while proc.poll() is None:
708
+ # Gather subprocess memory usage.
709
+ try:
710
+ pproc = psutil.Process(proc.pid)
711
+ proc_mem_bytes = pproc.memory_full_info().uss
712
+ for child in pproc.children(recursive=True):
713
+ proc_mem_bytes += child.memory_full_info().uss
714
+ max_mem_bytes = max(max_mem_bytes, proc_mem_bytes)
715
+
716
+ memory_usage = psutil.virtual_memory()
717
+ if memory_usage.percent > MEMORY_WARN_LIMIT:
718
+ self.__logger.warn(
719
+ 'Current system memory usage is '
720
+ f'{memory_usage.percent:.1f}%')
721
+
722
+ # increase limit warning
723
+ MEMORY_WARN_LIMIT = int(memory_usage.percent + 1)
724
+ except psutil.Error:
725
+ # Process may have already terminated or been killed.
726
+ # Retain existing memory usage statistics in this case.
727
+ pass
728
+ except PermissionError:
729
+ # OS is preventing access to this information so it cannot
730
+ # be collected
731
+ pass
732
+
733
+ # Loop until process terminates
734
+ read_stdio(stdout_reader, stderr_reader)
735
+
736
+ duration = time.time() - cpu_start
737
+ if timeout is not None and duration > timeout:
738
+ raise TaskTimeout(timeout=duration)
739
+
740
+ time.sleep(POLL_INTERVAL)
741
+ except KeyboardInterrupt:
742
+ self.__logger.info("Received ctrl-c.")
743
+ self.__terminate_exe(proc)
744
+ raise TaskError
745
+ except TaskTimeout as e:
746
+ self.__logger.error(f'Task timed out after {e.timeout:.1f} seconds')
747
+ self.__terminate_exe(proc)
748
+ raise e from None
749
+
750
+ # Read the remaining io
751
+ read_stdio(stdout_reader, stderr_reader)
752
+
753
+ retcode = proc.returncode
754
+
755
+ # Record record information
756
+ self.schema("record").record_tool(
757
+ self.__step, self.__index,
758
+ retcode, RecordTool.EXITCODE)
759
+
760
+ # Capture runtime metrics
761
+ self.schema("metric").record(
762
+ self.__step, self.__index,
763
+ 'exetime', time.time() - cpu_start, unit='s')
764
+ self.schema("metric").record(
765
+ self.__step, self.__index,
766
+ 'memory', max_mem_bytes, unit='B')
767
+
768
+ return retcode
769
+
770
+ def __getstate__(self):
771
+ state = self.__dict__.copy()
772
+
773
+ # Remove runtime information
774
+ for key in list(state.keys()):
775
+ if key.startswith("_ToolSchema__"):
776
+ del state[key]
777
+ return state
778
+
779
+ def __setstate__(self, state):
780
+ self.__dict__ = state
781
+
782
+ # Reinit runtime information
783
+ self.set_runtime(None)
784
+
785
+ ###############################################################
786
+ def parse_version(self, stdout):
787
+ raise NotImplementedError("must be implemented by the implementation class")
788
+
789
+ def normalize_version(self, version):
790
+ return version
791
+
792
+ def setup(self):
793
+ pass
794
+
795
+ def pre_process(self):
796
+ pass
797
+
798
+ def runtime_options(self):
799
+ return []
800
+
801
+ def run(self):
802
+ raise NotImplementedError("must be implemented by the implementation class")
803
+
804
+ def post_process(self):
805
+ pass
806
+
807
+
808
+ ###########################################################################
809
+ # Tool Setup
810
+ ###########################################################################
811
+ def schema_tool(schema):
812
+ schema = EditableSchema(schema)
813
+
814
+ schema.insert(
815
+ 'exe',
816
+ Parameter(
817
+ 'str',
818
+ scope=Scope.GLOBAL,
819
+ shorthelp="Tool: executable name",
820
+ switch="-tool_exe 'tool <str>'",
821
+ example=["cli: -tool_exe 'openroad openroad'",
822
+ "api: chip.set('tool', 'openroad', 'exe', 'openroad')"],
823
+ help=trim("""Tool executable name.""")))
824
+
825
+ schema.insert(
826
+ 'sbom', 'default',
827
+ Parameter(
828
+ '[file]',
829
+ scope=Scope.GLOBAL,
830
+ pernode=PerNode.OPTIONAL,
831
+ shorthelp="Tool: software BOM",
832
+ switch="-tool_sbom 'tool version <file>'",
833
+ example=[
834
+ "cli: -tool_sbom 'yosys 1.0.1 ys_sbom.json'",
835
+ "api: chip.set('tool', 'yosys', 'sbom', '1.0', 'ys_sbom.json')"],
836
+ help=trim("""
837
+ Paths to software bill of material (SBOM) document file of the tool
838
+ specified on a per version basis. The SBOM includes critical
839
+ package information about the tool including the list of included
840
+ components, licenses, and copyright. The SBOM file is generally
841
+ provided as in a a standardized open data format such as SPDX.""")))
842
+
843
+ schema.insert(
844
+ 'path',
845
+ Parameter(
846
+ 'dir',
847
+ scope=Scope.GLOBAL,
848
+ pernode=PerNode.OPTIONAL,
849
+ shorthelp="Tool: executable path",
850
+ switch="-tool_path 'tool <dir>'",
851
+ example=[
852
+ "cli: -tool_path 'openroad /usr/local/bin'",
853
+ "api: chip.set('tool', 'openroad', 'path', '/usr/local/bin')"],
854
+ help=trim("""
855
+ File system path to tool executable. The path is prepended to the
856
+ system PATH environment variable for batch and interactive runs. The
857
+ path parameter can be left blank if the :keypath:`tool,<tool>,exe` is already in the
858
+ environment search path.""")))
859
+
860
+ schema.insert(
861
+ 'vswitch',
862
+ Parameter(
863
+ '[str]',
864
+ scope=Scope.GLOBAL,
865
+ shorthelp="Tool: executable version switch",
866
+ switch="-tool_vswitch 'tool <str>'",
867
+ example=["cli: -tool_vswitch 'openroad -version'",
868
+ "api: chip.set('tool', 'openroad', 'vswitch', '-version')"],
869
+ help=trim("""
870
+ Command line switch to use with executable used to print out
871
+ the version number. Common switches include ``-v``, ``-version``,
872
+ ``--version``. Some tools may require extra flags to run in batch mode.""")))
873
+
874
+ schema.insert(
875
+ 'vendor',
876
+ Parameter(
877
+ 'str',
878
+ scope=Scope.GLOBAL,
879
+ shorthelp="Tool: vendor",
880
+ switch="-tool_vendor 'tool <str>'",
881
+ example=["cli: -tool_vendor 'yosys yosys'",
882
+ "api: chip.set('tool', 'yosys', 'vendor', 'yosys')"],
883
+ help=trim("""
884
+ Name of the tool vendor. Parameter can be used to set vendor
885
+ specific technology variables in the PDK and libraries. For
886
+ open source projects, the project name should be used in
887
+ place of vendor.""")))
888
+
889
+ schema.insert(
890
+ 'version',
891
+ Parameter(
892
+ '[str]',
893
+ scope=Scope.GLOBAL,
894
+ pernode=PerNode.OPTIONAL,
895
+ shorthelp="Tool: version",
896
+ switch="-tool_version 'tool <str>'",
897
+ example=["cli: -tool_version 'openroad >=v2.0'",
898
+ "api: chip.set('tool', 'openroad', 'version', '>=v2.0')"],
899
+ help=trim("""
900
+ List of acceptable versions of the tool executable to be used. Each
901
+ entry in this list must be a version specifier as described by Python
902
+ `PEP-440 <https://peps.python.org/pep-0440/#version-specifiers>`_.
903
+ During task execution, the tool is called with the 'vswitch' to
904
+ check the runtime executable version. If the version of the system
905
+ executable is not allowed by any of the specifiers in 'version',
906
+ then the job is halted pre-execution. For backwards compatibility,
907
+ entries that do not conform to the standard will be interpreted as a
908
+ version with an '==' specifier. This check can be disabled by
909
+ setting :keypath:`option,novercheck` to True.""")))
910
+
911
+ schema.insert(
912
+ 'format',
913
+ Parameter(
914
+ '<json,tcl,yaml>',
915
+ scope=Scope.GLOBAL,
916
+ shorthelp="Tool: file format",
917
+ switch="-tool_format 'tool <str>'",
918
+ example=["cli: -tool_format 'yosys tcl'",
919
+ "api: chip.set('tool', 'yosys', 'format', 'tcl')"],
920
+ help=trim("""
921
+ File format for tool manifest handoff.""")))
922
+
923
+ schema.insert(
924
+ 'licenseserver', 'default',
925
+ Parameter(
926
+ '[str]',
927
+ scope=Scope.GLOBAL,
928
+ pernode=PerNode.OPTIONAL,
929
+ shorthelp="Tool: license servers",
930
+ switch="-tool_licenseserver 'name key <str>'",
931
+ example=[
932
+ "cli: -tool_licenseserver 'atask ACME_LICENSE 1700@server'",
933
+ "api: chip.set('tool', 'acme', 'licenseserver', 'ACME_LICENSE', '1700@server')"],
934
+ help=trim("""
935
+ Defines a set of tool specific environment variables used by the executable
936
+ that depend on license key servers to control access. For multiple servers,
937
+ separate each server by a 'colon'. The named license variable are read at
938
+ runtime (:meth:`.run()`) and the environment variables are set.
939
+ """)))
940
+
941
+
942
+ def schema_task(schema):
943
+ schema = EditableSchema(schema)
944
+
945
+ schema.insert(
946
+ 'warningoff',
947
+ Parameter(
948
+ '[str]',
949
+ scope=Scope.JOB,
950
+ pernode=PerNode.OPTIONAL,
951
+ shorthelp="Task: warning filter",
952
+ switch="-tool_task_warningoff 'tool task <str>'",
953
+ example=[
954
+ "cli: -tool_task_warningoff 'verilator lint COMBDLY'",
955
+ "api: chip.set('tool', 'verilator', 'task', 'lint', 'warningoff', 'COMBDLY')"],
956
+ help=trim("""
957
+ A list of tool warnings for which printing should be suppressed.
958
+ Generally this is done on a per design basis after review has
959
+ determined that warning can be safely ignored The code for turning
960
+ off warnings can be found in the specific task reference manual.
961
+ """)))
962
+
963
+ schema.insert(
964
+ 'regex', 'default',
965
+ Parameter(
966
+ '[str]',
967
+ scope=Scope.JOB,
968
+ pernode=PerNode.OPTIONAL,
969
+ shorthelp="Task: regex filter",
970
+ switch="-tool_task_regex 'tool task suffix <str>'",
971
+ example=[
972
+ "cli: -tool_task_regex 'openroad place errors \"'-v ERROR'\"'",
973
+ "api: chip.set('tool', 'openroad', 'task', 'place', 'regex', 'errors', "
974
+ "'-v ERROR')"],
975
+ help=trim("""
976
+ A list of piped together grep commands. Each entry represents a set
977
+ of command line arguments for grep including the regex pattern to
978
+ match. Starting with the first list entry, each grep output is piped
979
+ into the following grep command in the list. Supported grep options
980
+ include ``-v`` and ``-e``. Patterns starting with "-" should be
981
+ directly preceded by the ``-e`` option. The following example
982
+ illustrates the concept.
983
+
984
+ UNIX grep:
985
+
986
+ .. code-block:: bash
987
+
988
+ $ grep WARNING place.log | grep -v "bbox" > place.warnings
989
+
990
+ SiliconCompiler::
991
+
992
+ chip.set('task', 'openroad', 'regex', 'place', '0', 'warnings',
993
+ ["WARNING", "-v bbox"])
994
+
995
+ The "errors" and "warnings" suffixes are special cases. When set,
996
+ the number of matches found for these regexes will be added to the
997
+ errors and warnings metrics for the task, respectively. This will
998
+ also cause the logfile to be added to the :keypath:`tool, <tool>,
999
+ task, <task>, report` parameter for those metrics, if not already present.""")))
1000
+
1001
+ # Configuration: cli-option, tcl var, env var, file
1002
+ schema.insert(
1003
+ 'option',
1004
+ Parameter(
1005
+ '[str]',
1006
+ scope=Scope.JOB,
1007
+ pernode=PerNode.OPTIONAL,
1008
+ shorthelp="Task: executable options",
1009
+ switch="-tool_task_option 'tool task <str>'",
1010
+ example=[
1011
+ "cli: -tool_task_option 'openroad cts -no_init'",
1012
+ "api: chip.set('tool', 'openroad', 'task', 'cts', 'option', '-no_init')"],
1013
+ help=trim("""
1014
+ List of command line options for the task executable, specified on
1015
+ a per task and per step basis. Options must not include spaces.
1016
+ For multiple argument options, each option is a separate list element.
1017
+ """)))
1018
+
1019
+ schema.insert(
1020
+ 'var', 'default',
1021
+ Parameter(
1022
+ '[str]',
1023
+ scope=Scope.JOB,
1024
+ pernode=PerNode.OPTIONAL,
1025
+ shorthelp="Task: script variables",
1026
+ switch="-tool_task_var 'tool task key <str>'",
1027
+ example=[
1028
+ "cli: -tool_task_var 'openroad cts myvar 42'",
1029
+ "api: chip.set('tool', 'openroad', 'task', 'cts', 'var', 'myvar', '42')"],
1030
+ help=trim("""
1031
+ Task script variables specified as key value pairs. Variable
1032
+ names and value types must match the name and type of task and reference
1033
+ script consuming the variable.""")))
1034
+
1035
+ schema.insert(
1036
+ 'env', 'default',
1037
+ Parameter(
1038
+ 'str',
1039
+ scope=Scope.JOB,
1040
+ pernode=PerNode.OPTIONAL,
1041
+ shorthelp="Task: environment variables",
1042
+ switch="-tool_task_env 'tool task env <str>'",
1043
+ example=[
1044
+ "cli: -tool_task_env 'openroad cts MYVAR 42'",
1045
+ "api: chip.set('tool', 'openroad', 'task', 'cts', 'env', 'MYVAR', '42')"],
1046
+ help=trim("""
1047
+ Environment variables to set for individual tasks. Keys and values
1048
+ should be set in accordance with the task's documentation. Most
1049
+ tasks do not require extra environment variables to function.""")))
1050
+
1051
+ schema.insert(
1052
+ 'file', 'default', Parameter(
1053
+ '[file]',
1054
+ scope=Scope.JOB,
1055
+ pernode=PerNode.OPTIONAL,
1056
+ copy=True,
1057
+ shorthelp="Task: custom setup files",
1058
+ switch="-tool_task_file 'tool task key <file>'",
1059
+ example=[
1060
+ "cli: -tool_task_file 'openroad floorplan macroplace macroplace.tcl'",
1061
+ "api: chip.set('tool', 'openroad', 'task', 'floorplan', 'file', 'macroplace', "
1062
+ "'macroplace.tcl')"],
1063
+ help=trim("""
1064
+ Paths to user supplied files mapped to keys. Keys and filetypes must
1065
+ match what's expected by the task/reference script consuming the
1066
+ file.
1067
+ """)))
1068
+
1069
+ schema.insert(
1070
+ 'dir', 'default',
1071
+ Parameter(
1072
+ '[dir]',
1073
+ scope=Scope.JOB,
1074
+ pernode=PerNode.OPTIONAL,
1075
+ copy=True,
1076
+ shorthelp="Task: custom setup directories",
1077
+ switch="-tool_task_dir 'tool task key <dir>'",
1078
+ example=[
1079
+ "cli: -tool_task_dir 'verilator compile cincludes include'",
1080
+ "api: chip.set('tool', 'verilator', 'task', 'compile', 'dir', 'cincludes', "
1081
+ "'include')"],
1082
+ help=trim("""
1083
+ Paths to user supplied directories mapped to keys. Keys must match
1084
+ what's expected by the task/reference script consuming the
1085
+ directory.
1086
+ """)))
1087
+
1088
+ # Definitions of inputs, outputs, requirements
1089
+ schema.insert(
1090
+ 'input',
1091
+ Parameter(
1092
+ '[file]',
1093
+ scope=Scope.JOB,
1094
+ pernode=PerNode.REQUIRED,
1095
+ shorthelp="Task: input files",
1096
+ switch="-tool_task_input 'tool task <file>'",
1097
+ example=[
1098
+ "cli: -tool_task_input 'openroad place \"place 0 oh_add.def\"'",
1099
+ "api: chip.set('tool', 'openroad', 'task', 'place', 'input', 'oh_add.def', "
1100
+ "step='place', index='0')"],
1101
+ help=trim("""
1102
+ List of data files to be copied from previous flowgraph steps 'output'
1103
+ directory. The list of steps to copy files from is defined by the
1104
+ list defined by the dictionary key :keypath:`flowgraph,<flow>,<step>,<index>,input`.
1105
+ All files must be available for flow to continue. If a file
1106
+ is missing, the program exists on an error.""")))
1107
+
1108
+ schema.insert(
1109
+ 'output',
1110
+ Parameter(
1111
+ '[file]',
1112
+ scope=Scope.JOB,
1113
+ pernode=PerNode.REQUIRED,
1114
+ shorthelp="Task: output files",
1115
+ switch="-tool_task_output 'tool task <file>'",
1116
+ example=[
1117
+ "cli: -tool_task_output 'openroad place \"place 0 oh_add.def\"'",
1118
+ "api: chip.set('tool', 'openroad', 'task', 'place', 'output', 'oh_add.def', "
1119
+ "step='place', index='0')"],
1120
+ help=trim("""
1121
+ List of data files written to the 'output' directory of the
1122
+ tool/task/step/index used in the keypath. All files must be available
1123
+ for flow to continue. If a file is missing, the program exists on an error.""")))
1124
+
1125
+ dest_enum = ['log', 'output', 'none']
1126
+ schema.insert(
1127
+ 'stdout', 'destination',
1128
+ Parameter(
1129
+ f'<{",".join(dest_enum)}>',
1130
+ defvalue='log',
1131
+ scope=Scope.JOB,
1132
+ pernode=PerNode.OPTIONAL,
1133
+ shorthelp="Task: destination for stdout",
1134
+ switch="-tool_task_stdout_destination 'tool task <str>'",
1135
+ example=["cli: -tool_task_stdout_destination 'ghdl import log'",
1136
+ "api: chip.set('tool', 'ghdl', 'task', 'import', 'stdout', 'destination', "
1137
+ "'log')"],
1138
+ help=trim("""
1139
+ Defines where to direct the output generated over stdout.
1140
+ Supported options are:
1141
+ none: the stream generated to STDOUT is ignored.
1142
+ log: the generated stream is stored in <step>.<suffix>; if not in quiet mode,
1143
+ it is additionally dumped to the display.
1144
+ output: the generated stream is stored in outputs/<design>.<suffix>.""")))
1145
+
1146
+ schema.insert(
1147
+ 'stdout', 'suffix',
1148
+ Parameter(
1149
+ 'str',
1150
+ defvalue='log',
1151
+ scope=Scope.JOB,
1152
+ pernode=PerNode.OPTIONAL,
1153
+ shorthelp="Task: file suffix for redirected stdout",
1154
+ switch="-tool_task_stdout_suffix 'tool task <str>'",
1155
+ example=["cli: -tool_task_stdout_suffix 'ghdl import log'",
1156
+ "api: chip.set('tool', ghdl', 'task', 'import', 'stdout', 'suffix', 'log')"],
1157
+ help=trim("""
1158
+ Specifies the file extension for the content redirected from stdout.""")))
1159
+
1160
+ schema.insert(
1161
+ 'stderr', 'destination',
1162
+ Parameter(
1163
+ f'<{",".join(dest_enum)}>',
1164
+ defvalue='log',
1165
+ scope=Scope.JOB,
1166
+ pernode=PerNode.OPTIONAL,
1167
+ shorthelp="Task: destination for stderr",
1168
+ switch="-tool_task_stderr_destination 'tool task <str>'",
1169
+ example=["cli: -tool_task_stderr_destination 'ghdl import log'",
1170
+ "api: chip.set('tool', ghdl', 'task', 'import', 'stderr', 'destination', "
1171
+ "'log')"],
1172
+ help=trim("""
1173
+ Defines where to direct the output generated over stderr.
1174
+ Supported options are:
1175
+ none: the stream generated to STDERR is ignored
1176
+ log: the generated stream is stored in <step>.<suffix>; if not in quiet mode,
1177
+ it is additionally dumped to the display.
1178
+ output: the generated stream is stored in outputs/<design>.<suffix>""")))
1179
+
1180
+ schema.insert(
1181
+ 'stderr', 'suffix',
1182
+ Parameter(
1183
+ 'str',
1184
+ defvalue='log',
1185
+ scope=Scope.JOB,
1186
+ pernode=PerNode.OPTIONAL,
1187
+ shorthelp="Task: file suffix for redirected stderr",
1188
+ switch="-tool_task_stderr_suffix 'tool task <str>'",
1189
+ example=["cli: -tool_task_stderr_suffix 'ghdl import log'",
1190
+ "api: chip.set('tool', 'ghdl', 'task', 'import', 'stderr', 'suffix', 'log')"],
1191
+ help=trim("""
1192
+ Specifies the file extension for the content redirected from stderr.""")))
1193
+
1194
+ schema.insert(
1195
+ 'require',
1196
+ Parameter(
1197
+ '[str]',
1198
+ scope=Scope.JOB,
1199
+ pernode=PerNode.OPTIONAL,
1200
+ shorthelp="Task: parameter requirements",
1201
+ switch="-tool_task_require 'tool task <str>'",
1202
+ example=[
1203
+ "cli: -tool_task_require 'openroad cts design'",
1204
+ "api: chip.set('tool', 'openroad', 'task', 'cts', 'require', 'design')"],
1205
+ help=trim("""
1206
+ List of keypaths to required task parameters. The list is used
1207
+ by :meth:`.check_manifest()` to verify that all parameters have been set up before
1208
+ step execution begins.""")))
1209
+
1210
+ schema.insert(
1211
+ 'report', 'default',
1212
+ Parameter(
1213
+ '[file]',
1214
+ scope=Scope.JOB,
1215
+ pernode=PerNode.REQUIRED,
1216
+ shorthelp="Task: metric report files",
1217
+ switch="-tool_task_report 'tool task metric <file>'",
1218
+ example=[
1219
+ "cli: -tool_task_report 'openroad place holdtns \"place 0 place.log\"'",
1220
+ "api: chip.set('tool', 'openroad', 'task', 'place', 'report', 'holdtns', "
1221
+ "'place.log', step='place', index='0')"],
1222
+ help=trim("""
1223
+ List of report files associated with a specific 'metric'. The file path
1224
+ specified is relative to the run directory of the current task.""")))
1225
+
1226
+ schema.insert(
1227
+ 'refdir',
1228
+ Parameter(
1229
+ '[dir]',
1230
+ scope=Scope.JOB,
1231
+ pernode=PerNode.OPTIONAL,
1232
+ shorthelp="Task: script directory",
1233
+ switch="-tool_task_refdir 'tool task <dir>'",
1234
+ example=[
1235
+ "cli: -tool_task_refdir 'yosys syn ./myref'",
1236
+ "api: chip.set('tool', 'yosys', 'task', 'syn_asic', 'refdir', './myref')"],
1237
+ help=trim("""
1238
+ Path to directories containing reference flow scripts, specified
1239
+ on a per step and index basis.""")))
1240
+
1241
+ schema.insert(
1242
+ 'script',
1243
+ Parameter(
1244
+ '[file]',
1245
+ scope=Scope.JOB,
1246
+ pernode=PerNode.OPTIONAL,
1247
+ shorthelp="Task: entry script",
1248
+ switch="-tool_task_script 'tool task <file>'",
1249
+ example=[
1250
+ "cli: -tool_task_script 'yosys syn syn.tcl'",
1251
+ "api: chip.set('tool', 'yosys', 'task', 'syn_asic', 'script', 'syn.tcl')"],
1252
+ help=trim("""
1253
+ Path to the entry script called by the executable specified
1254
+ on a per task and per step basis.""")))
1255
+
1256
+ schema.insert(
1257
+ 'prescript',
1258
+ Parameter(
1259
+ '[file]',
1260
+ scope=Scope.JOB,
1261
+ pernode=PerNode.OPTIONAL,
1262
+ copy=True,
1263
+ shorthelp="Task: pre-step script",
1264
+ switch="-tool_task_prescript 'tool task <file>'",
1265
+ example=[
1266
+ "cli: -tool_task_prescript 'yosys syn syn_pre.tcl'",
1267
+ "api: chip.set('tool', 'yosys', 'task', 'syn_asic', 'prescript', 'syn_pre.tcl')"],
1268
+ help=trim("""
1269
+ Path to a user supplied script to execute after reading in the design
1270
+ but before the main execution stage of the step. Exact entry point
1271
+ depends on the step and main script being executed. An example
1272
+ of a prescript entry point would be immediately before global
1273
+ placement.""")))
1274
+
1275
+ schema.insert(
1276
+ 'postscript',
1277
+ Parameter(
1278
+ '[file]',
1279
+ scope=Scope.JOB,
1280
+ pernode=PerNode.OPTIONAL,
1281
+ copy=True,
1282
+ shorthelp="Task: post-step script",
1283
+ switch="-tool_task_postscript 'tool task <file>'",
1284
+ example=[
1285
+ "cli: -tool_task_postscript 'yosys syn syn_post.tcl'",
1286
+ "api: chip.set('tool', 'yosys', 'task', 'syn_asic', 'postscript', 'syn_post.tcl')"],
1287
+ help=trim("""
1288
+ Path to a user supplied script to execute after the main execution
1289
+ stage of the step but before the design is saved.
1290
+ Exact entry point depends on the step and main script being
1291
+ executed. An example of a postscript entry point would be immediately
1292
+ after global placement.""")))
1293
+
1294
+ schema.insert(
1295
+ 'threads',
1296
+ Parameter(
1297
+ 'int',
1298
+ scope=Scope.JOB,
1299
+ pernode=PerNode.OPTIONAL,
1300
+ shorthelp="Task: thread parallelism",
1301
+ switch="-tool_task_threads 'tool task <int>'",
1302
+ example=["cli: -tool_task_threads 'magic drc 64'",
1303
+ "api: chip.set('tool', 'magic', 'task', 'drc', 'threads', '64')"],
1304
+ help=trim("""
1305
+ Thread parallelism to use for execution specified on a per task and per
1306
+ step basis. If not specified, SC queries the operating system and sets
1307
+ the threads based on the maximum thread count supported by the
1308
+ hardware.""")))