siliconcompiler 0.26.5__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 (251) hide show
  1. siliconcompiler/__init__.py +24 -0
  2. siliconcompiler/__main__.py +12 -0
  3. siliconcompiler/_common.py +49 -0
  4. siliconcompiler/_metadata.py +36 -0
  5. siliconcompiler/apps/__init__.py +0 -0
  6. siliconcompiler/apps/_common.py +76 -0
  7. siliconcompiler/apps/sc.py +92 -0
  8. siliconcompiler/apps/sc_dashboard.py +94 -0
  9. siliconcompiler/apps/sc_issue.py +178 -0
  10. siliconcompiler/apps/sc_remote.py +199 -0
  11. siliconcompiler/apps/sc_server.py +39 -0
  12. siliconcompiler/apps/sc_show.py +142 -0
  13. siliconcompiler/apps/smake.py +232 -0
  14. siliconcompiler/checklists/__init__.py +0 -0
  15. siliconcompiler/checklists/oh_tapeout.py +41 -0
  16. siliconcompiler/core.py +3221 -0
  17. siliconcompiler/data/RobotoMono/LICENSE.txt +202 -0
  18. siliconcompiler/data/RobotoMono/RobotoMono-Regular.ttf +0 -0
  19. siliconcompiler/data/heartbeat.v +18 -0
  20. siliconcompiler/data/logo.png +0 -0
  21. siliconcompiler/flowgraph.py +570 -0
  22. siliconcompiler/flows/__init__.py +0 -0
  23. siliconcompiler/flows/_common.py +67 -0
  24. siliconcompiler/flows/asicflow.py +180 -0
  25. siliconcompiler/flows/asictopflow.py +38 -0
  26. siliconcompiler/flows/dvflow.py +86 -0
  27. siliconcompiler/flows/fpgaflow.py +202 -0
  28. siliconcompiler/flows/generate_openroad_rcx.py +66 -0
  29. siliconcompiler/flows/lintflow.py +35 -0
  30. siliconcompiler/flows/screenshotflow.py +51 -0
  31. siliconcompiler/flows/showflow.py +59 -0
  32. siliconcompiler/flows/signoffflow.py +53 -0
  33. siliconcompiler/flows/synflow.py +128 -0
  34. siliconcompiler/fpgas/__init__.py +0 -0
  35. siliconcompiler/fpgas/lattice_ice40.py +42 -0
  36. siliconcompiler/fpgas/vpr_example.py +109 -0
  37. siliconcompiler/issue.py +300 -0
  38. siliconcompiler/libs/__init__.py +0 -0
  39. siliconcompiler/libs/asap7sc7p5t.py +8 -0
  40. siliconcompiler/libs/gf180mcu.py +8 -0
  41. siliconcompiler/libs/nangate45.py +8 -0
  42. siliconcompiler/libs/sky130hd.py +8 -0
  43. siliconcompiler/libs/sky130io.py +8 -0
  44. siliconcompiler/package.py +412 -0
  45. siliconcompiler/pdks/__init__.py +0 -0
  46. siliconcompiler/pdks/asap7.py +8 -0
  47. siliconcompiler/pdks/freepdk45.py +8 -0
  48. siliconcompiler/pdks/gf180.py +8 -0
  49. siliconcompiler/pdks/skywater130.py +8 -0
  50. siliconcompiler/remote/__init__.py +36 -0
  51. siliconcompiler/remote/client.py +891 -0
  52. siliconcompiler/remote/schema.py +106 -0
  53. siliconcompiler/remote/server.py +507 -0
  54. siliconcompiler/remote/server_schema/requests/cancel_job.json +51 -0
  55. siliconcompiler/remote/server_schema/requests/check_progress.json +61 -0
  56. siliconcompiler/remote/server_schema/requests/check_server.json +38 -0
  57. siliconcompiler/remote/server_schema/requests/delete_job.json +51 -0
  58. siliconcompiler/remote/server_schema/requests/get_results.json +48 -0
  59. siliconcompiler/remote/server_schema/requests/remote_run.json +40 -0
  60. siliconcompiler/remote/server_schema/responses/cancel_job.json +18 -0
  61. siliconcompiler/remote/server_schema/responses/check_progress.json +30 -0
  62. siliconcompiler/remote/server_schema/responses/check_server.json +32 -0
  63. siliconcompiler/remote/server_schema/responses/delete_job.json +18 -0
  64. siliconcompiler/remote/server_schema/responses/get_results.json +21 -0
  65. siliconcompiler/remote/server_schema/responses/remote_run.json +25 -0
  66. siliconcompiler/report/__init__.py +13 -0
  67. siliconcompiler/report/html_report.py +74 -0
  68. siliconcompiler/report/report.py +355 -0
  69. siliconcompiler/report/streamlit_report.py +137 -0
  70. siliconcompiler/report/streamlit_viewer.py +944 -0
  71. siliconcompiler/report/summary_image.py +117 -0
  72. siliconcompiler/report/summary_table.py +105 -0
  73. siliconcompiler/report/utils.py +163 -0
  74. siliconcompiler/scheduler/__init__.py +2092 -0
  75. siliconcompiler/scheduler/docker_runner.py +253 -0
  76. siliconcompiler/scheduler/run_node.py +138 -0
  77. siliconcompiler/scheduler/send_messages.py +178 -0
  78. siliconcompiler/scheduler/slurm.py +208 -0
  79. siliconcompiler/scheduler/validation/email_credentials.json +54 -0
  80. siliconcompiler/schema/__init__.py +7 -0
  81. siliconcompiler/schema/schema_cfg.py +4014 -0
  82. siliconcompiler/schema/schema_obj.py +1841 -0
  83. siliconcompiler/schema/utils.py +93 -0
  84. siliconcompiler/sphinx_ext/__init__.py +0 -0
  85. siliconcompiler/sphinx_ext/dynamicgen.py +1006 -0
  86. siliconcompiler/sphinx_ext/schemagen.py +221 -0
  87. siliconcompiler/sphinx_ext/utils.py +166 -0
  88. siliconcompiler/targets/__init__.py +0 -0
  89. siliconcompiler/targets/asap7_demo.py +68 -0
  90. siliconcompiler/targets/asic_demo.py +38 -0
  91. siliconcompiler/targets/fpgaflow_demo.py +47 -0
  92. siliconcompiler/targets/freepdk45_demo.py +59 -0
  93. siliconcompiler/targets/gf180_demo.py +77 -0
  94. siliconcompiler/targets/skywater130_demo.py +70 -0
  95. siliconcompiler/templates/email/general.j2 +66 -0
  96. siliconcompiler/templates/email/summary.j2 +43 -0
  97. siliconcompiler/templates/issue/README.txt +26 -0
  98. siliconcompiler/templates/issue/run.sh +6 -0
  99. siliconcompiler/templates/report/bootstrap.min.css +7 -0
  100. siliconcompiler/templates/report/bootstrap.min.js +7 -0
  101. siliconcompiler/templates/report/bootstrap_LICENSE.md +24 -0
  102. siliconcompiler/templates/report/sc_report.j2 +427 -0
  103. siliconcompiler/templates/slurm/run.sh +9 -0
  104. siliconcompiler/templates/tcl/manifest.tcl.j2 +137 -0
  105. siliconcompiler/tools/__init__.py +0 -0
  106. siliconcompiler/tools/_common/__init__.py +432 -0
  107. siliconcompiler/tools/_common/asic.py +115 -0
  108. siliconcompiler/tools/_common/sdc/sc_constraints.sdc +76 -0
  109. siliconcompiler/tools/_common/tcl/sc_pin_constraints.tcl +63 -0
  110. siliconcompiler/tools/bambu/bambu.py +32 -0
  111. siliconcompiler/tools/bambu/convert.py +77 -0
  112. siliconcompiler/tools/bluespec/bluespec.py +40 -0
  113. siliconcompiler/tools/bluespec/convert.py +103 -0
  114. siliconcompiler/tools/builtin/_common.py +155 -0
  115. siliconcompiler/tools/builtin/builtin.py +26 -0
  116. siliconcompiler/tools/builtin/concatenate.py +85 -0
  117. siliconcompiler/tools/builtin/join.py +27 -0
  118. siliconcompiler/tools/builtin/maximum.py +46 -0
  119. siliconcompiler/tools/builtin/minimum.py +57 -0
  120. siliconcompiler/tools/builtin/mux.py +70 -0
  121. siliconcompiler/tools/builtin/nop.py +38 -0
  122. siliconcompiler/tools/builtin/verify.py +83 -0
  123. siliconcompiler/tools/chisel/SCDriver.scala +10 -0
  124. siliconcompiler/tools/chisel/build.sbt +27 -0
  125. siliconcompiler/tools/chisel/chisel.py +37 -0
  126. siliconcompiler/tools/chisel/convert.py +140 -0
  127. siliconcompiler/tools/execute/exec_input.py +41 -0
  128. siliconcompiler/tools/execute/execute.py +17 -0
  129. siliconcompiler/tools/genfasm/bitstream.py +61 -0
  130. siliconcompiler/tools/genfasm/genfasm.py +40 -0
  131. siliconcompiler/tools/ghdl/convert.py +87 -0
  132. siliconcompiler/tools/ghdl/ghdl.py +41 -0
  133. siliconcompiler/tools/icarus/compile.py +87 -0
  134. siliconcompiler/tools/icarus/icarus.py +36 -0
  135. siliconcompiler/tools/icepack/bitstream.py +20 -0
  136. siliconcompiler/tools/icepack/icepack.py +43 -0
  137. siliconcompiler/tools/klayout/export.py +117 -0
  138. siliconcompiler/tools/klayout/klayout.py +119 -0
  139. siliconcompiler/tools/klayout/klayout_export.py +205 -0
  140. siliconcompiler/tools/klayout/klayout_operations.py +363 -0
  141. siliconcompiler/tools/klayout/klayout_show.py +242 -0
  142. siliconcompiler/tools/klayout/klayout_utils.py +176 -0
  143. siliconcompiler/tools/klayout/operations.py +194 -0
  144. siliconcompiler/tools/klayout/screenshot.py +98 -0
  145. siliconcompiler/tools/klayout/show.py +101 -0
  146. siliconcompiler/tools/magic/drc.py +49 -0
  147. siliconcompiler/tools/magic/extspice.py +19 -0
  148. siliconcompiler/tools/magic/magic.py +85 -0
  149. siliconcompiler/tools/magic/sc_drc.tcl +96 -0
  150. siliconcompiler/tools/magic/sc_extspice.tcl +54 -0
  151. siliconcompiler/tools/magic/sc_magic.tcl +47 -0
  152. siliconcompiler/tools/montage/montage.py +30 -0
  153. siliconcompiler/tools/montage/tile.py +66 -0
  154. siliconcompiler/tools/netgen/count_lvs.py +132 -0
  155. siliconcompiler/tools/netgen/lvs.py +90 -0
  156. siliconcompiler/tools/netgen/netgen.py +36 -0
  157. siliconcompiler/tools/netgen/sc_lvs.tcl +46 -0
  158. siliconcompiler/tools/nextpnr/apr.py +24 -0
  159. siliconcompiler/tools/nextpnr/nextpnr.py +59 -0
  160. siliconcompiler/tools/openfpgaloader/openfpgaloader.py +39 -0
  161. siliconcompiler/tools/openroad/__init__.py +0 -0
  162. siliconcompiler/tools/openroad/cts.py +45 -0
  163. siliconcompiler/tools/openroad/dfm.py +66 -0
  164. siliconcompiler/tools/openroad/export.py +131 -0
  165. siliconcompiler/tools/openroad/floorplan.py +70 -0
  166. siliconcompiler/tools/openroad/openroad.py +977 -0
  167. siliconcompiler/tools/openroad/physyn.py +27 -0
  168. siliconcompiler/tools/openroad/place.py +41 -0
  169. siliconcompiler/tools/openroad/rcx_bench.py +95 -0
  170. siliconcompiler/tools/openroad/rcx_extract.py +34 -0
  171. siliconcompiler/tools/openroad/route.py +45 -0
  172. siliconcompiler/tools/openroad/screenshot.py +60 -0
  173. siliconcompiler/tools/openroad/scripts/sc_apr.tcl +499 -0
  174. siliconcompiler/tools/openroad/scripts/sc_cts.tcl +64 -0
  175. siliconcompiler/tools/openroad/scripts/sc_dfm.tcl +20 -0
  176. siliconcompiler/tools/openroad/scripts/sc_export.tcl +98 -0
  177. siliconcompiler/tools/openroad/scripts/sc_floorplan.tcl +413 -0
  178. siliconcompiler/tools/openroad/scripts/sc_metrics.tcl +158 -0
  179. siliconcompiler/tools/openroad/scripts/sc_physyn.tcl +7 -0
  180. siliconcompiler/tools/openroad/scripts/sc_place.tcl +84 -0
  181. siliconcompiler/tools/openroad/scripts/sc_procs.tcl +423 -0
  182. siliconcompiler/tools/openroad/scripts/sc_rcx.tcl +63 -0
  183. siliconcompiler/tools/openroad/scripts/sc_rcx_bench.tcl +20 -0
  184. siliconcompiler/tools/openroad/scripts/sc_rcx_extract.tcl +12 -0
  185. siliconcompiler/tools/openroad/scripts/sc_route.tcl +133 -0
  186. siliconcompiler/tools/openroad/scripts/sc_screenshot.tcl +21 -0
  187. siliconcompiler/tools/openroad/scripts/sc_write.tcl +5 -0
  188. siliconcompiler/tools/openroad/scripts/sc_write_images.tcl +361 -0
  189. siliconcompiler/tools/openroad/show.py +94 -0
  190. siliconcompiler/tools/openroad/templates/pex.tcl +8 -0
  191. siliconcompiler/tools/opensta/__init__.py +101 -0
  192. siliconcompiler/tools/opensta/report_libraries.py +28 -0
  193. siliconcompiler/tools/opensta/scripts/sc_procs.tcl +47 -0
  194. siliconcompiler/tools/opensta/scripts/sc_report_libraries.tcl +74 -0
  195. siliconcompiler/tools/opensta/scripts/sc_timing.tcl +268 -0
  196. siliconcompiler/tools/opensta/timing.py +214 -0
  197. siliconcompiler/tools/slang/__init__.py +49 -0
  198. siliconcompiler/tools/slang/lint.py +101 -0
  199. siliconcompiler/tools/surelog/__init__.py +123 -0
  200. siliconcompiler/tools/surelog/parse.py +183 -0
  201. siliconcompiler/tools/surelog/templates/output.v +7 -0
  202. siliconcompiler/tools/sv2v/convert.py +46 -0
  203. siliconcompiler/tools/sv2v/sv2v.py +37 -0
  204. siliconcompiler/tools/template/template.py +125 -0
  205. siliconcompiler/tools/verilator/compile.py +139 -0
  206. siliconcompiler/tools/verilator/lint.py +19 -0
  207. siliconcompiler/tools/verilator/parse.py +27 -0
  208. siliconcompiler/tools/verilator/verilator.py +172 -0
  209. siliconcompiler/tools/vivado/__init__.py +7 -0
  210. siliconcompiler/tools/vivado/bitstream.py +21 -0
  211. siliconcompiler/tools/vivado/place.py +21 -0
  212. siliconcompiler/tools/vivado/route.py +21 -0
  213. siliconcompiler/tools/vivado/scripts/sc_bitstream.tcl +6 -0
  214. siliconcompiler/tools/vivado/scripts/sc_place.tcl +2 -0
  215. siliconcompiler/tools/vivado/scripts/sc_route.tcl +4 -0
  216. siliconcompiler/tools/vivado/scripts/sc_run.tcl +45 -0
  217. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +25 -0
  218. siliconcompiler/tools/vivado/syn_fpga.py +20 -0
  219. siliconcompiler/tools/vivado/vivado.py +147 -0
  220. siliconcompiler/tools/vpr/_json_constraint.py +63 -0
  221. siliconcompiler/tools/vpr/_xml_constraint.py +109 -0
  222. siliconcompiler/tools/vpr/place.py +137 -0
  223. siliconcompiler/tools/vpr/route.py +124 -0
  224. siliconcompiler/tools/vpr/screenshot.py +54 -0
  225. siliconcompiler/tools/vpr/show.py +88 -0
  226. siliconcompiler/tools/vpr/vpr.py +357 -0
  227. siliconcompiler/tools/xyce/xyce.py +36 -0
  228. siliconcompiler/tools/yosys/lec.py +56 -0
  229. siliconcompiler/tools/yosys/prepareLib.py +59 -0
  230. siliconcompiler/tools/yosys/sc_lec.tcl +84 -0
  231. siliconcompiler/tools/yosys/sc_syn.tcl +79 -0
  232. siliconcompiler/tools/yosys/syn_asic.py +565 -0
  233. siliconcompiler/tools/yosys/syn_asic.tcl +377 -0
  234. siliconcompiler/tools/yosys/syn_asic_fpga_shared.tcl +31 -0
  235. siliconcompiler/tools/yosys/syn_fpga.py +146 -0
  236. siliconcompiler/tools/yosys/syn_fpga.tcl +233 -0
  237. siliconcompiler/tools/yosys/syn_strategies.tcl +81 -0
  238. siliconcompiler/tools/yosys/techmaps/lcu_kogge_stone.v +39 -0
  239. siliconcompiler/tools/yosys/templates/abc.const +2 -0
  240. siliconcompiler/tools/yosys/yosys.py +147 -0
  241. siliconcompiler/units.py +259 -0
  242. siliconcompiler/use.py +177 -0
  243. siliconcompiler/utils/__init__.py +423 -0
  244. siliconcompiler/utils/asic.py +158 -0
  245. siliconcompiler/utils/showtools.py +25 -0
  246. siliconcompiler-0.26.5.dist-info/LICENSE +190 -0
  247. siliconcompiler-0.26.5.dist-info/METADATA +195 -0
  248. siliconcompiler-0.26.5.dist-info/RECORD +251 -0
  249. siliconcompiler-0.26.5.dist-info/WHEEL +5 -0
  250. siliconcompiler-0.26.5.dist-info/entry_points.txt +12 -0
  251. siliconcompiler-0.26.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1841 @@
1
+ # Copyright 2022 Silicon Compiler Authors. All Rights Reserved.
2
+
3
+ # NOTE: this file cannot rely on any third-party dependencies, including other
4
+ # SC dependencies outside of its directory, since it may be used by tool drivers
5
+ # that have isolated Python environments.
6
+
7
+ import copy
8
+ import json
9
+ import logging
10
+ import os
11
+ import re
12
+ import pathlib
13
+ import argparse
14
+ import sys
15
+ import shlex
16
+
17
+ try:
18
+ import gzip
19
+ _has_gzip = True
20
+ except ModuleNotFoundError:
21
+ _has_gzip = False
22
+
23
+ try:
24
+ import csv
25
+ _has_csv = True
26
+ except ModuleNotFoundError:
27
+ _has_csv = False
28
+
29
+ try:
30
+ import yaml
31
+ _has_yaml = True
32
+ except ImportError:
33
+ _has_yaml = False
34
+
35
+ from .schema_cfg import schema_cfg
36
+ from .utils import escape_val_tcl, PACKAGE_ROOT, translate_loglevel
37
+
38
+
39
+ class Schema:
40
+ """Object for storing and accessing configuration values corresponding to
41
+ the SiliconCompiler schema.
42
+
43
+ Most user-facing interaction with the schema should occur through an
44
+ instance of :class:`~siliconcompiler.core.Chip`, but this class is available
45
+ for schema manipulation tasks that don't require the additional context of a
46
+ Chip object.
47
+
48
+ The two arguments to this class are mutually exclusive. If neither are
49
+ provided, the object is initialized to default values for all parameters.
50
+
51
+ Args:
52
+ cfg (dict): Initial configuration dictionary. This may be a subtree of
53
+ the schema.
54
+ manifest (str): Initial manifest.
55
+ logger (logging.Logger): instance of the parent logger if available
56
+ """
57
+
58
+ # Special key in node dict that represents a value corresponds to a
59
+ # global default for all steps/indices.
60
+ GLOBAL_KEY = 'global'
61
+ PERNODE_FIELDS = ('value', 'filehash', 'date', 'author', 'signature', 'package')
62
+
63
+ def __init__(self, cfg=None, manifest=None, logger=None):
64
+ if cfg is not None and manifest is not None:
65
+ raise ValueError('You may not specify both cfg and manifest')
66
+
67
+ self._init_logger(logger)
68
+
69
+ self._stop_journal()
70
+
71
+ if manifest is not None:
72
+ # Normalize value to string in case we receive a pathlib.Path
73
+ cfg, self.__journal = Schema.__read_manifest_file(str(manifest))
74
+ else:
75
+ cfg = copy.deepcopy(cfg)
76
+
77
+ if cfg is not None:
78
+ try:
79
+ if Schema.__dict_requires_normalization(cfg):
80
+ cfg = Schema.__dict_to_schema(cfg)
81
+ self.cfg = cfg
82
+ except (TypeError, ValueError) as e:
83
+ raise ValueError('Attempting to read manifest with '
84
+ f'incompatible schema version: {e}') \
85
+ from e
86
+ else:
87
+ self.cfg = self._init_schema_cfg()
88
+
89
+ ###########################################################################
90
+ def _init_schema_cfg(self):
91
+ return schema_cfg()
92
+
93
+ ###########################################################################
94
+ @staticmethod
95
+ def __dict_to_schema_set(cfg, *key):
96
+ if Schema._is_leaf(cfg):
97
+ for field, value in cfg.items():
98
+ if field == 'node':
99
+ for step, substep in value.items():
100
+ if step == 'default':
101
+ continue
102
+ for index, values in substep.items():
103
+ if step == Schema.GLOBAL_KEY:
104
+ sstep = None
105
+ else:
106
+ sstep = step
107
+ if index == Schema.GLOBAL_KEY:
108
+ sindex = None
109
+ else:
110
+ sindex = index
111
+ for nodefield, nodevalue in values.items():
112
+ Schema.__set(*key, nodevalue,
113
+ cfg=cfg,
114
+ field=nodefield,
115
+ step=sstep, index=sindex)
116
+ else:
117
+ Schema.__set(*key, value, cfg=cfg, field=field)
118
+ else:
119
+ for nextkey, subcfg in cfg.items():
120
+ Schema.__dict_to_schema_set(subcfg, *key, nextkey)
121
+
122
+ ###########################################################################
123
+ @staticmethod
124
+ def __dict_to_schema(cfg):
125
+ for category, subcfg in cfg.items():
126
+ if category in ('history', 'library'):
127
+ # History and library are subschemas
128
+ for _, value in subcfg.items():
129
+ Schema.__dict_to_schema(value)
130
+ else:
131
+ Schema.__dict_to_schema_set(subcfg, category)
132
+ return cfg
133
+
134
+ ###########################################################################
135
+ @staticmethod
136
+ def __dict_requires_normalization(cfg):
137
+ '''
138
+ Recurse over scheme configuration to check for tuples
139
+ Returns: False if dict is correct, True is dict requires normalization,
140
+ None if tuples were not found
141
+ '''
142
+ if Schema._is_leaf(cfg):
143
+ if '(' in cfg['type']:
144
+ for step, substep in cfg['node'].items():
145
+ for index, values in substep.items():
146
+ values = values['value']
147
+ if not values:
148
+ continue
149
+ if isinstance(values, list):
150
+ for v in values:
151
+ if isinstance(v, tuple):
152
+ return False
153
+ if isinstance(values, tuple):
154
+ return False
155
+ return True
156
+ else:
157
+ return None
158
+ else:
159
+ for subcfg in cfg.values():
160
+ ret = Schema.__dict_requires_normalization(subcfg)
161
+ if ret is None:
162
+ continue
163
+ else:
164
+ return ret
165
+
166
+ ###########################################################################
167
+ def _merge_with_init_schema(self):
168
+ new_schema = Schema()
169
+
170
+ for keylist in self.allkeys():
171
+ if keylist[0] in ('history', 'library'):
172
+ continue
173
+
174
+ if 'default' in keylist:
175
+ continue
176
+
177
+ # only read in valid keypaths without 'default'
178
+ key_valid = new_schema.valid(*keylist, default_valid=True)
179
+ if not key_valid:
180
+ self.logger.warning(f'Keypath {keylist} is not valid')
181
+ if not key_valid:
182
+ continue
183
+
184
+ for val, step, index in self._getvals(*keylist, return_defvalue=False):
185
+ new_schema.set(*keylist, val, step=step, index=index)
186
+
187
+ # update other pernode fields
188
+ # TODO: only update these if clobber is successful
189
+ step_key = Schema.GLOBAL_KEY if not step else step
190
+ idx_key = Schema.GLOBAL_KEY if not index else index
191
+ for field in self.getdict(*keylist)['node'][step_key][idx_key].keys():
192
+ if field == 'value':
193
+ continue
194
+ new_schema.set(*keylist,
195
+ self.get(*keylist, step=step, index=index, field=field),
196
+ step=step, index=index, field=field)
197
+
198
+ if 'library' in self.cfg:
199
+ # Handle libraries separately
200
+ for library in self.cfg['library'].keys():
201
+ lib_schema = Schema(cfg=self.getdict('library', library))
202
+ lib_schema._merge_with_init_schema()
203
+ new_schema.cfg['library'][library] = lib_schema.cfg
204
+
205
+ if 'history' in self.cfg:
206
+ # Copy over history
207
+ new_schema.cfg['history'] = self.cfg['history']
208
+
209
+ self.cfg = new_schema.cfg
210
+
211
+ ###########################################################################
212
+ @staticmethod
213
+ def __read_manifest_file(filepath):
214
+ if not os.path.isfile(filepath):
215
+ raise ValueError(f'Manifest file not found {filepath}')
216
+
217
+ if os.path.splitext(filepath)[1].lower() == '.gz':
218
+ if not _has_gzip:
219
+ raise RuntimeError("gzip is not available")
220
+ fin = gzip.open(filepath, 'r')
221
+ else:
222
+ fin = open(filepath, 'r')
223
+
224
+ try:
225
+ if re.search(r'(\.json|\.sup)(\.gz)*$', filepath, flags=re.IGNORECASE):
226
+ localcfg = json.load(fin)
227
+ elif re.search(r'(\.yaml|\.yml)(\.gz)*$', filepath, flags=re.IGNORECASE):
228
+ if not _has_yaml:
229
+ raise ImportError('yaml package required to read YAML manifest')
230
+ localcfg = yaml.load(fin, Loader=yaml.SafeLoader)
231
+ else:
232
+ raise ValueError(f'File format not recognized {filepath}')
233
+ finally:
234
+ fin.close()
235
+
236
+ journal = None
237
+ try:
238
+ if '__journal__' in localcfg:
239
+ journal = localcfg['__journal__']
240
+ del localcfg['__journal__']
241
+ except (TypeError, ValueError) as e:
242
+ raise ValueError(f'Attempting to read manifest with incompatible schema version: {e}') \
243
+ from e
244
+
245
+ return localcfg, journal
246
+
247
+ def get(self, *keypath, field='value', job=None, step=None, index=None):
248
+ """
249
+ Returns a schema parameter field.
250
+
251
+ See :meth:`~siliconcompiler.core.Chip.get` for detailed documentation.
252
+ """
253
+ # Prevent accidental modifications of the schema content by not passing a reference
254
+ return copy.copy(self.__get(*keypath, field=field, job=job, step=step, index=index))
255
+
256
+ ###########################################################################
257
+ def __get(self, *keypath, field='value', job=None, step=None, index=None):
258
+ cfg = self.__search(*keypath, job=job)
259
+
260
+ if not Schema._is_leaf(cfg):
261
+ raise ValueError(f'Invalid keypath {keypath}: get() '
262
+ 'must be called on a complete keypath')
263
+
264
+ err = Schema.__validate_step_index(cfg['pernode'], field, step, index)
265
+ if err:
266
+ raise ValueError(f'Invalid args to get() of keypath {keypath}: {err}')
267
+
268
+ if isinstance(index, int):
269
+ index = str(index)
270
+
271
+ if field in self.PERNODE_FIELDS:
272
+ try:
273
+ return cfg['node'][step][index][field]
274
+ except KeyError:
275
+ if cfg['pernode'] == 'required':
276
+ return cfg['node']['default']['default'][field]
277
+
278
+ try:
279
+ return cfg['node'][step][self.GLOBAL_KEY][field]
280
+ except KeyError:
281
+ pass
282
+
283
+ try:
284
+ return cfg['node'][self.GLOBAL_KEY][self.GLOBAL_KEY][field]
285
+ except KeyError:
286
+ return cfg['node']['default']['default'][field]
287
+ elif field in cfg:
288
+ return cfg[field]
289
+ else:
290
+ raise ValueError(f'Invalid field {field}')
291
+
292
+ ###########################################################################
293
+ def set(self, *args, field='value', clobber=True, step=None, index=None):
294
+ '''
295
+ Sets a schema parameter field.
296
+
297
+ See :meth:`~siliconcompiler.core.Chip.set` for detailed documentation.
298
+ '''
299
+
300
+ keypath = args[:-1]
301
+ cfg = self.__search(*keypath, insert_defaults=True)
302
+
303
+ return self.__set(*args, logger=self.logger, cfg=cfg, field=field, clobber=clobber,
304
+ step=step, index=index, journal_callback=self.__record_journal)
305
+
306
+ ###########################################################################
307
+ @staticmethod
308
+ def __set(*args, logger=None, cfg=None, field='value', clobber=True,
309
+ step=None, index=None,
310
+ journal_callback=None):
311
+ '''
312
+ Sets a schema parameter field.
313
+
314
+ See :meth:`~siliconcompiler.core.Chip.set` for detailed documentation.
315
+ '''
316
+ keypath = args[:-1]
317
+ value = args[-1]
318
+
319
+ if not Schema._is_leaf(cfg):
320
+ raise ValueError(f'Invalid keypath {keypath}: set() '
321
+ 'must be called on a complete keypath')
322
+
323
+ err = Schema.__validate_step_index(cfg['pernode'], field, step, index)
324
+ if err:
325
+ raise ValueError(f'Invalid args to set() of keypath {keypath}: {err}')
326
+
327
+ if isinstance(index, int):
328
+ index = str(index)
329
+
330
+ if cfg['lock'] and field != 'lock':
331
+ if logger:
332
+ logger.debug(f'Failed to set value for {keypath}: parameter is locked')
333
+ return False
334
+
335
+ if Schema.__is_set(cfg, step=step, index=index) and not clobber:
336
+ if logger:
337
+ logger.debug(f'Failed to set value for {keypath}: clobber is False '
338
+ 'and parameter is set')
339
+ return False
340
+
341
+ allowed_values = None
342
+ if 'enum' in cfg:
343
+ allowed_values = cfg['enum']
344
+
345
+ value = Schema.__check_and_normalize(value, cfg['type'], field, keypath, allowed_values)
346
+
347
+ if journal_callback:
348
+ journal_callback("set", keypath,
349
+ value=value,
350
+ field=field,
351
+ step=step, index=index)
352
+
353
+ if field in Schema.PERNODE_FIELDS:
354
+ step = step if step is not None else Schema.GLOBAL_KEY
355
+ index = index if index is not None else Schema.GLOBAL_KEY
356
+
357
+ if step not in cfg['node']:
358
+ cfg['node'][step] = {}
359
+ if index not in cfg['node'][step]:
360
+ cfg['node'][step][index] = copy.deepcopy(cfg['node']['default']['default'])
361
+ cfg['node'][step][index][field] = value
362
+ else:
363
+ cfg[field] = value
364
+
365
+ return True
366
+
367
+ ###########################################################################
368
+ def add(self, *args, field='value', step=None, index=None):
369
+ '''
370
+ Adds item(s) to a schema parameter list.
371
+
372
+ See :meth:`~siliconcompiler.core.Chip.add` for detailed documentation.
373
+ '''
374
+ keypath = args[:-1]
375
+
376
+ cfg = self.__search(*keypath, insert_defaults=True)
377
+
378
+ return self._add(*args, cfg=cfg, field=field, step=step, index=index)
379
+
380
+ ###########################################################################
381
+ def _add(self, *args, cfg=None, field='value', step=None, index=None, package=None):
382
+ '''
383
+ Adds item(s) to a schema parameter list.
384
+
385
+ See :meth:`~siliconcompiler.core.Chip.add` for detailed documentation.
386
+ '''
387
+ keypath = args[:-1]
388
+ value = args[-1]
389
+
390
+ if not Schema._is_leaf(cfg):
391
+ raise ValueError(f'Invalid keypath {keypath}: add() '
392
+ 'must be called on a complete keypath')
393
+
394
+ err = Schema.__validate_step_index(cfg['pernode'], field, step, index)
395
+ if err:
396
+ raise ValueError(f'Invalid args to add() of keypath {keypath}: {err}')
397
+
398
+ if isinstance(index, int):
399
+ index = str(index)
400
+
401
+ if not Schema.__is_list(field, cfg['type']):
402
+ if field == 'value':
403
+ raise ValueError(f'Invalid keypath {keypath}: add() must be called on a list')
404
+ else:
405
+ raise ValueError(f'Invalid field {field}: add() must be called on a list')
406
+
407
+ if cfg['lock']:
408
+ self.logger.debug(f'Failed to set value for {keypath}: parameter is locked')
409
+ return False
410
+
411
+ allowed_values = None
412
+ if 'enum' in cfg:
413
+ allowed_values = cfg['enum']
414
+
415
+ value = Schema.__check_and_normalize(value, cfg['type'], field, keypath, allowed_values)
416
+ self.__record_journal("add", keypath, value=value, field=field, step=step, index=index)
417
+ if field in self.PERNODE_FIELDS:
418
+ modified_step = step if step is not None else self.GLOBAL_KEY
419
+ modified_index = index if index is not None else self.GLOBAL_KEY
420
+
421
+ if modified_step not in cfg['node']:
422
+ cfg['node'][modified_step] = {}
423
+ if modified_index not in cfg['node'][modified_step]:
424
+ cfg['node'][modified_step][modified_index] = copy.deepcopy(
425
+ cfg['node']['default']['default'])
426
+ cfg['node'][modified_step][modified_index][field].extend(value)
427
+ else:
428
+ cfg[field].extend(value)
429
+
430
+ return True
431
+
432
+ ###########################################################################
433
+ def change_type(self, *key, type=None):
434
+ '''
435
+ Change the type of a key
436
+
437
+ Args:
438
+ key (list): Key to change.
439
+ type (str): New data type for this key
440
+
441
+ Examples:
442
+ >>> chip.set('option', 'var', 'run_test', 'true')
443
+ >>> chip.schema.change_type('option', 'var', 'run_test', 'bool')
444
+ Changes the type of ['option', 'var', 'run_test'] to a boolean.
445
+ '''
446
+
447
+ if not type:
448
+ raise ValueError('Type cannot be empty')
449
+
450
+ if 'file' in type or 'dir' in type:
451
+ raise ValueError(f'Cannot convert to {type}')
452
+
453
+ cfg = self.__search(*key, insert_defaults=True)
454
+ if not Schema._is_leaf(cfg):
455
+ raise ValueError(f'Invalid keypath {key}: change_type() '
456
+ 'must be called on a complete keypath')
457
+
458
+ old_type = self.get(*key, field='type')
459
+ if 'file' in old_type or 'dir' in old_type:
460
+ raise ValueError(f'Cannot convert from {old_type}')
461
+
462
+ old_type_is_list = '[' in old_type
463
+ new_type_is_list = '[' in type
464
+
465
+ if 'file' in old_type or 'dir' in old_type:
466
+ raise ValueError(f'Cannot convert from {type}')
467
+
468
+ new_values = []
469
+ for values, step, index in [*self._getvals(*key),
470
+ (self.get_default(*key), 'default', 'default')]:
471
+ if old_type_is_list and not new_type_is_list:
472
+ # Old type is list, but new type in not a list
473
+ # Can only convert if list has 1 or 0 elements
474
+ if len(values) > 1:
475
+ raise ValueError(f'Too many values in {",".join(key)} to convert a '
476
+ 'list of a scalar.')
477
+ if len(values) == 1:
478
+ values = values[0]
479
+ else:
480
+ values = None
481
+
482
+ if new_type_is_list and values is None:
483
+ values = []
484
+
485
+ new_values.append((step, index, values))
486
+
487
+ self.set(*key, type, field='type')
488
+ for step, index, values in new_values:
489
+ if step == 'default' and index == 'default':
490
+ self.set_default(*key, values)
491
+ else:
492
+ self.set(*key, values, step=step, index=index)
493
+
494
+ ###########################################################################
495
+ def remove(self, *keypath):
496
+ '''
497
+ Remove a keypath
498
+
499
+ See :meth:`~siliconcompiler.core.Chip.remove` for detailed documentation.
500
+ '''
501
+ search_path = keypath[0:-1]
502
+ removal_key = keypath[-1]
503
+
504
+ if removal_key == 'default':
505
+ self.logger.error(f'Cannot remove default keypath: {keypath}')
506
+ return
507
+
508
+ cfg = self.__search(*search_path)
509
+ if 'default' not in cfg:
510
+ self.logger.error(f'Cannot remove a non-default keypath: {keypath}')
511
+ return
512
+
513
+ if removal_key not in cfg:
514
+ self.logger.error(f'Key does not exist: {keypath}')
515
+ return
516
+
517
+ for key in self.allkeys(*keypath):
518
+ fullpath = [*keypath, *key]
519
+ if self.get(*fullpath, field='lock'):
520
+ self.logger.error(f'Key is locked: {fullpath}')
521
+ return
522
+
523
+ del cfg[removal_key]
524
+ self.__record_journal("remove", keypath)
525
+
526
+ ###########################################################################
527
+ def unset(self, *keypath, step=None, index=None):
528
+ '''
529
+ Unsets a schema parameter field.
530
+
531
+ See :meth:`~siliconcompiler.core.Chip.unset` for detailed documentation.
532
+ '''
533
+ cfg = self.__search(*keypath)
534
+
535
+ if not Schema._is_leaf(cfg):
536
+ raise ValueError(f'Invalid keypath {keypath}: unset() '
537
+ 'must be called on a complete keypath')
538
+
539
+ err = Schema.__validate_step_index(cfg['pernode'], 'value', step, index)
540
+ if err:
541
+ raise ValueError(f'Invalid args to unset() of keypath {keypath}: {err}')
542
+
543
+ if isinstance(index, int):
544
+ index = str(index)
545
+
546
+ if cfg['lock']:
547
+ self.logger.debug(f'Failed to set value for {keypath}: parameter is locked')
548
+ return False
549
+
550
+ if step is None:
551
+ step = Schema.GLOBAL_KEY
552
+ if index is None:
553
+ index = Schema.GLOBAL_KEY
554
+
555
+ try:
556
+ del cfg['node'][step][index]
557
+ self.__record_journal("unset", keypath, step=step, index=index)
558
+ except KeyError:
559
+ # If this key doesn't exist, silently continue - it was never set
560
+ pass
561
+
562
+ return True
563
+
564
+ def _getvals(self, *keypath, return_defvalue=True):
565
+ """
566
+ Returns all values (global and pernode) associated with a particular parameter.
567
+
568
+ Returns a list of tuples of the form (value, step, index). The list is
569
+ in no particular order. For the global value, step and index are None.
570
+ If return_defvalue is True, the default parameter value is added to the
571
+ list in place of a global value if a global value is not set.
572
+ """
573
+ cfg = self.__search(*keypath)
574
+
575
+ if not Schema._is_leaf(cfg):
576
+ raise ValueError(f'Invalid keypath {keypath}: _getvals() '
577
+ 'must be called on a complete keypath')
578
+
579
+ vals = []
580
+ has_global = False
581
+ for step in cfg['node']:
582
+ if step == 'default':
583
+ continue
584
+
585
+ for index in cfg['node'][step]:
586
+ step_arg = None if step == self.GLOBAL_KEY else step
587
+ index_arg = None if index == self.GLOBAL_KEY else index
588
+ if 'value' in cfg['node'][step][index]:
589
+ if step_arg is None and index_arg is None:
590
+ has_global = True
591
+ vals.append((cfg['node'][step][index]['value'], step_arg, index_arg))
592
+
593
+ if (cfg['pernode'] != 'required') and not has_global and return_defvalue:
594
+ vals.append((cfg['node']['default']['default']['value'], None, None))
595
+
596
+ return vals
597
+
598
+ ###########################################################################
599
+ def getkeys(self, *keypath, job=None):
600
+ """
601
+ Returns a list of schema dictionary keys.
602
+
603
+ See :meth:`~siliconcompiler.core.Chip.getkeys` for detailed documentation.
604
+ """
605
+ cfg = self.__search(*keypath, job=job)
606
+ keys = list(cfg.keys())
607
+
608
+ if 'default' in keys:
609
+ keys.remove('default')
610
+
611
+ return keys
612
+
613
+ ###########################################################################
614
+ def getdict(self, *keypath):
615
+ """
616
+ Returns a schema dictionary.
617
+
618
+ See :meth:`~siliconcompiler.core.Chip.getdict` for detailed
619
+ documentation.
620
+ """
621
+ cfg = self.__search(*keypath)
622
+ return copy.deepcopy(cfg)
623
+
624
+ ###########################################################################
625
+ def valid(self, *args, default_valid=False, job=None):
626
+ """
627
+ Checks validity of a keypath.
628
+
629
+ See :meth:`~siliconcompiler.core.Chip.valid` for detailed
630
+ documentation.
631
+ """
632
+ keylist = list(args)
633
+ if default_valid:
634
+ default = 'default'
635
+ else:
636
+ default = None
637
+
638
+ if job is not None:
639
+ cfg = self.cfg['history'][job]
640
+ else:
641
+ cfg = self.cfg
642
+
643
+ for key in keylist:
644
+ if key in cfg:
645
+ cfg = cfg[key]
646
+ elif default_valid and default in cfg:
647
+ cfg = cfg[default]
648
+ else:
649
+ return False
650
+ return Schema._is_leaf(cfg)
651
+
652
+ ##########################################################################
653
+ def has_field(self, *args):
654
+ keypath = args[:-1]
655
+ field = args[-1]
656
+
657
+ cfg = self.__search(*keypath)
658
+ return field in cfg
659
+
660
+ ##########################################################################
661
+ def record_history(self):
662
+ '''
663
+ Copies all non-empty parameters from current job into the history
664
+ dictionary.
665
+ '''
666
+
667
+ # initialize new dict
668
+ jobname = self.get('option', 'jobname')
669
+ self.cfg['history'][jobname] = {}
670
+
671
+ # copy in all empty values of scope job
672
+ allkeys = self.allkeys()
673
+ for key in allkeys:
674
+ # ignore history in case of cumulative history
675
+ if key[0] != 'history':
676
+ scope = self.get(*key, field='scope')
677
+ if not self.is_empty(*key) and (scope == 'job'):
678
+ self.__copyparam(self.cfg,
679
+ self.cfg['history'][jobname],
680
+ key)
681
+
682
+ @staticmethod
683
+ def __check_and_normalize(value, sc_type, field, keypath, allowed_values):
684
+ '''
685
+ This method validates that user-provided values match the expected type,
686
+ and returns a normalized version of the value.
687
+
688
+ The expected type is based on the schema parameter type string for
689
+ value-related fields, and is based on the field itself for other fields.
690
+ This function raises a TypeError if an illegal value is provided.
691
+
692
+ The normalization process provides some leeway in how users supply
693
+ values, while ensuring that values are stored consistently in the schema.
694
+
695
+ The normalization rules are as follows:
696
+ - If a scalar is provided for a list type, it is promoted to a list of
697
+ one element.
698
+ - If a list is provided for a tuple type, it is cast to a tuple (since
699
+ the JSON module serializes tuples as arrays, which are deserialized into
700
+ lists).
701
+ - Elements inside lists and tuples are normalized recursively.
702
+ - All non-list values have a string representation that gets cast to a
703
+ native Python type (since we receive strings from the CLI):
704
+ - bool: accepts "true" or "false"
705
+ - ints and floats: cast as if by int() or float()
706
+ - tuples: accepts comma-separated values surrounded by parens
707
+ '''
708
+
709
+ if value is None and not Schema.__is_list(field, sc_type):
710
+ # None is legal for all scalars, but not within collection types
711
+ # TODO: could consider normalizing "None" for lists to empty list?
712
+ return value
713
+
714
+ if field == 'value':
715
+ # Push down error_msg from the top since arguments get modified in recursive call
716
+ error_msg = f'Invalid value {value} for keypath {keypath}: expected type {sc_type}'
717
+ return Schema._normalize_value(value, sc_type, error_msg, allowed_values)
718
+ else:
719
+ return Schema.__normalize_field(value, sc_type, field, keypath)
720
+
721
+ @staticmethod
722
+ def _normalize_value(value, sc_type, error_msg, allowed_values):
723
+ if sc_type.startswith('['):
724
+ base_type = sc_type[1:-1]
725
+
726
+ # Need to try 2 different recursion strategies - if value is a list already, then we can
727
+ # recurse on it directly. However, if that doesn't work, then it might be a
728
+ # list-of-lists/tuples that needs to be wrapped in an outer list, so we try that.
729
+ if isinstance(value, (list, set, tuple)):
730
+ try:
731
+ return [Schema._normalize_value(v, base_type, error_msg, allowed_values)
732
+ for v in value]
733
+ except TypeError:
734
+ pass
735
+
736
+ value = [value]
737
+ return [Schema._normalize_value(v, base_type, error_msg, allowed_values) for v in value]
738
+
739
+ if sc_type.startswith('('):
740
+ # TODO: make parsing more robust to support tuples-of-tuples
741
+ if isinstance(value, str):
742
+ value = value[1:-1].split(',')
743
+ elif not (isinstance(value, tuple) or isinstance(value, list)):
744
+ raise TypeError(error_msg)
745
+
746
+ base_types = sc_type[1:-1].split(',')
747
+ if len(value) != len(base_types):
748
+ raise TypeError(error_msg)
749
+ return tuple(Schema._normalize_value(v, base_type, error_msg, allowed_values)
750
+ for v, base_type in zip(value, base_types))
751
+
752
+ if sc_type == 'bool':
753
+ if value == 'true':
754
+ return True
755
+ if value == 'false':
756
+ return False
757
+ if isinstance(value, bool):
758
+ return value
759
+ if isinstance(value, (int, float)):
760
+ return value != 0
761
+ raise TypeError(error_msg)
762
+
763
+ try:
764
+ if sc_type == 'int':
765
+ return int(value)
766
+
767
+ if sc_type == 'float':
768
+ return float(value)
769
+ except TypeError:
770
+ raise TypeError(error_msg) from None
771
+
772
+ if sc_type == 'str':
773
+ if isinstance(value, str):
774
+ return value
775
+ elif isinstance(value, bool):
776
+ return str(value).lower()
777
+ elif isinstance(value, (list, tuple)):
778
+ raise TypeError(error_msg)
779
+ else:
780
+ return str(value)
781
+
782
+ if sc_type in ('file', 'dir'):
783
+ if isinstance(value, (str, pathlib.Path)):
784
+ return str(value)
785
+ else:
786
+ raise TypeError(error_msg)
787
+
788
+ if sc_type == 'enum':
789
+ if isinstance(value, str):
790
+ if value in allowed_values:
791
+ return value
792
+ valid = ", ".join(allowed_values)
793
+ raise ValueError(error_msg + f", and value of {valid}")
794
+ else:
795
+ raise TypeError(error_msg)
796
+
797
+ raise ValueError(f'Invalid type specifier: {sc_type}')
798
+
799
+ @staticmethod
800
+ def __normalize_field(value, sc_type, field, keypath):
801
+ def error_msg(t):
802
+ return f'Invalid value {value} for field {field} of keypath {keypath}: expected {t}'
803
+
804
+ if field in ('author', 'date') and ('file' not in sc_type):
805
+ raise TypeError(f'Invalid field {field} for keypath {keypath}: '
806
+ 'this field only exists for file parameters')
807
+
808
+ if field in ('copy', 'filehash', 'package', 'hashalgo') and \
809
+ ('file' not in sc_type and 'dir' not in sc_type):
810
+ raise TypeError(f'Invalid field {field} for keypath {keypath}: '
811
+ 'this field only exists for file and dir parameters')
812
+
813
+ is_list = Schema.__is_list(field, sc_type)
814
+ if field == 'package' and is_list:
815
+ if not isinstance(value, list):
816
+ value = [value]
817
+ if not all((v is None or isinstance(v, (str, pathlib.Path))) for v in value):
818
+ raise TypeError(error_msg('None, str or pathlib.Path'))
819
+ return value
820
+
821
+ if is_list:
822
+ if not value:
823
+ # Replace none with an empty list
824
+ value = []
825
+
826
+ if not isinstance(value, list):
827
+ value = [value]
828
+
829
+ if not all(isinstance(v, str) for v in value):
830
+ raise TypeError(error_msg('str'))
831
+ return value
832
+
833
+ if field == 'scope':
834
+ # Restricted allowed values
835
+ if not (isinstance(value, str) and value in ('global', 'job', 'scratch')):
836
+ raise TypeError(error_msg('one of "global", "job", or "scratch"'))
837
+ return value
838
+
839
+ if field == 'pernode':
840
+ # Restricted allowed values
841
+ if not (isinstance(value, str) and value in ('never', 'optional', 'required')):
842
+ raise TypeError(f'Invalid value {value} for field {field}: '
843
+ 'expected one of "never", "optional", or "required"')
844
+ return value
845
+
846
+ if field in (
847
+ 'type', 'switch', 'shorthelp', 'help', 'unit', 'hashalgo', 'notes',
848
+ 'signature'
849
+ ):
850
+ if not isinstance(value, str):
851
+ raise TypeError(error_msg('str'))
852
+ return value
853
+
854
+ if field in ('lock', 'copy', 'require'):
855
+ if value == 'true':
856
+ return True
857
+ if value == 'false':
858
+ return False
859
+ if isinstance(value, bool):
860
+ return value
861
+ else:
862
+ raise TypeError(error_msg('bool'))
863
+
864
+ if field in ('node',):
865
+ if isinstance(value, dict):
866
+ return value
867
+ else:
868
+ raise TypeError(f'Invalid value {value} for field {field}: expected dict')
869
+
870
+ raise ValueError(f'Invalid field {field} for keypath {keypath}')
871
+
872
+ @staticmethod
873
+ def __is_set(cfg, step=None, index=None):
874
+ '''Returns whether a user has set a value for this parameter.
875
+
876
+ A value counts as set if a user has set a global value OR a value for
877
+ the provided step/index.
878
+ '''
879
+ if Schema.GLOBAL_KEY in cfg['node'] and \
880
+ Schema.GLOBAL_KEY in cfg['node'][Schema.GLOBAL_KEY] and \
881
+ 'value' in cfg['node'][Schema.GLOBAL_KEY][Schema.GLOBAL_KEY]:
882
+ # global value is set
883
+ return True
884
+
885
+ if step is None:
886
+ return False
887
+ if index is None:
888
+ index = Schema.GLOBAL_KEY
889
+
890
+ return step in cfg['node'] and \
891
+ index in cfg['node'][step] and \
892
+ 'value' in cfg['node'][step][index]
893
+
894
+ @staticmethod
895
+ def _is_leaf(cfg):
896
+ # 'shorthelp' chosen arbitrarily: any mandatory field with a consistent
897
+ # type would work.
898
+ return 'shorthelp' in cfg and isinstance(cfg['shorthelp'], str)
899
+
900
+ @staticmethod
901
+ def __is_list(field, type):
902
+ if field in ('filehash', 'date', 'author', 'example', 'enum', 'switch', 'package'):
903
+ return True
904
+
905
+ is_list = type.startswith('[')
906
+ if is_list and field in ('signature', 'value'):
907
+ return True
908
+
909
+ return False
910
+
911
+ @staticmethod
912
+ def __validate_step_index(pernode, field, step, index):
913
+ '''Shared validation logic for the step and index keyword arguments to
914
+ get(), set(), and add(), based on the pernode setting of a parameter and
915
+ field.
916
+
917
+ Returns an error message if there's a problem with the arguments,
918
+ otherwise None.
919
+ '''
920
+ if field not in Schema.PERNODE_FIELDS:
921
+ if step is not None or index is not None:
922
+ return 'step and index are only valid for value fields'
923
+ return None
924
+
925
+ if pernode == 'never' and (step is not None or index is not None):
926
+ return 'step and index are not valid for this parameter'
927
+
928
+ if pernode == 'required' and (step is None or index is None):
929
+ return 'step and index are required for this parameter'
930
+
931
+ if step is None and index is not None:
932
+ return 'if index is provided, step must be provided as well'
933
+
934
+ # Step and index for default should be accessed set_/get_default
935
+ if step == 'default':
936
+ return f'illegal step name: {step} is reserved'
937
+
938
+ if index == 'default':
939
+ return f'illegal index name: {step} is reserved'
940
+
941
+ return None
942
+
943
+ def __search(self, *keypath, insert_defaults=False, job=None):
944
+ if job is not None:
945
+ cfg = self.cfg['history'][job]
946
+ else:
947
+ cfg = self.cfg
948
+
949
+ for key in keypath:
950
+ if not isinstance(key, str):
951
+ raise TypeError(f'Invalid keypath {keypath}: key is not a string: {key}')
952
+
953
+ if Schema._is_leaf(cfg):
954
+ raise ValueError(f'Invalid keypath {keypath}: unexpected key: {key}')
955
+
956
+ if key in cfg:
957
+ cfg = cfg[key]
958
+ elif 'default' in cfg:
959
+ if insert_defaults:
960
+ cfg[key] = copy.deepcopy(cfg['default'])
961
+ cfg = cfg[key]
962
+ else:
963
+ cfg = cfg['default']
964
+ else:
965
+ raise ValueError(f'Invalid keypath {keypath}: unexpected key: {key}')
966
+
967
+ return cfg
968
+
969
+ ###########################################################################
970
+ def allkeys(self, *keypath_prefix):
971
+ '''
972
+ Returns all keypaths in the schema as a list of lists.
973
+
974
+ See :meth:`~siliconcompiler.core.Chip.allkeys` for detailed documentation.
975
+ '''
976
+ if len(keypath_prefix) > 0:
977
+ return self.__allkeys(cfg=self.getdict(*keypath_prefix))
978
+ else:
979
+ return self.__allkeys()
980
+
981
+ ###########################################################################
982
+ def __allkeys(self, cfg=None, base_key=None):
983
+ if cfg is None:
984
+ cfg = self.cfg
985
+
986
+ if Schema._is_leaf(cfg):
987
+ return []
988
+
989
+ keylist = []
990
+ if base_key is None:
991
+ base_key = []
992
+ for k in cfg:
993
+ key = (*base_key, k)
994
+ if Schema._is_leaf(cfg[k]):
995
+ keylist.append(key)
996
+ else:
997
+ keylist.extend(self.__allkeys(cfg=cfg[k], base_key=key))
998
+ return keylist
999
+
1000
+ ###########################################################################
1001
+ def __copyparam(self, cfgsrc, cfgdst, keypath):
1002
+ '''
1003
+ Copies a parameter into the manifest history dictionary.
1004
+ '''
1005
+
1006
+ # 1. descend keypath, pop each key as its used
1007
+ # 2. create key if missing in destination dict
1008
+ # 3. populate leaf cell when keypath empty
1009
+ if keypath:
1010
+ keypath = list(keypath)
1011
+ key = keypath[0]
1012
+ keypath.pop(0)
1013
+ if key not in cfgdst.keys():
1014
+ cfgdst[key] = {}
1015
+ self.__copyparam(cfgsrc[key], cfgdst[key], keypath)
1016
+ else:
1017
+ for key in cfgsrc.keys():
1018
+ if key not in ('example', 'switch', 'help'):
1019
+ cfgdst[key] = copy.deepcopy(cfgsrc[key])
1020
+
1021
+ ###########################################################################
1022
+ def write_json(self, fout):
1023
+ localcfg = {**self.cfg}
1024
+ if self.__journal is not None:
1025
+ localcfg['__journal__'] = self.__journal
1026
+ fout.write(json.dumps(localcfg, indent=4))
1027
+
1028
+ ###########################################################################
1029
+ def write_yaml(self, fout):
1030
+ if not _has_yaml:
1031
+ raise ImportError('yaml package required to write YAML manifest')
1032
+ fout.write(yaml.dump(self.cfg, Dumper=YamlIndentDumper, default_flow_style=False))
1033
+
1034
+ ###########################################################################
1035
+ def write_tcl(self, fout, prefix="", step=None, index=None, template=None):
1036
+ '''
1037
+ Prints out schema as TCL dictionary
1038
+ '''
1039
+
1040
+ tcl_set_cmds = []
1041
+ for key in self.allkeys():
1042
+ typestr = self.get(*key, field='type')
1043
+ pernode = self.get(*key, field='pernode')
1044
+
1045
+ if pernode == 'required' and (step is None or index is None):
1046
+ # Skip mandatory per-node parameters if step and index are not specified
1047
+ # TODO: how should we dump these?
1048
+ continue
1049
+
1050
+ if pernode != 'never':
1051
+ value = self.get(*key, step=step, index=index)
1052
+ else:
1053
+ value = self.get(*key)
1054
+
1055
+ # create a TCL dict
1056
+ keystr = ' '.join([escape_val_tcl(keypart, 'str') for keypart in key])
1057
+
1058
+ valstr = escape_val_tcl(value, typestr)
1059
+
1060
+ # Turning scalars into lists
1061
+ if not (typestr.startswith('[') or typestr.startswith('(')):
1062
+ valstr = f'[list {valstr}]'
1063
+
1064
+ # TODO: Temp fix to get rid of empty args
1065
+ if valstr == '':
1066
+ valstr = '[list ]'
1067
+
1068
+ outstr = f"{prefix} {keystr} {valstr}"
1069
+
1070
+ # print out all non default values
1071
+ if 'default' not in key:
1072
+ tcl_set_cmds.append(outstr)
1073
+
1074
+ if template:
1075
+ fout.write(template.render(manifest_dict='\n'.join(tcl_set_cmds),
1076
+ scroot=os.path.abspath(PACKAGE_ROOT)))
1077
+ else:
1078
+ for cmd in tcl_set_cmds:
1079
+ fout.write(cmd + '\n')
1080
+ fout.write('\n')
1081
+
1082
+ ###########################################################################
1083
+ def write_csv(self, fout):
1084
+ if not _has_csv:
1085
+ raise RuntimeError("csv is not available")
1086
+
1087
+ csvwriter = csv.writer(fout)
1088
+ csvwriter.writerow(['Keypath', 'Value'])
1089
+
1090
+ allkeys = self.allkeys()
1091
+ for key in allkeys:
1092
+ keypath = ','.join(key)
1093
+ for value, step, index in self._getvals(*key):
1094
+ if step is None and index is None:
1095
+ keypath = ','.join(key)
1096
+ elif index is None:
1097
+ keypath = ','.join([*key, step, 'default'])
1098
+ else:
1099
+ keypath = ','.join([*key, step, index])
1100
+
1101
+ if isinstance(value, list):
1102
+ for item in value:
1103
+ csvwriter.writerow([keypath, item])
1104
+ else:
1105
+ csvwriter.writerow([keypath, value])
1106
+
1107
+ ###########################################################################
1108
+ def copy(self):
1109
+ '''Returns deep copy of Schema object.'''
1110
+ newscheme = Schema(cfg=self.cfg)
1111
+ if self.__journal:
1112
+ newscheme.__journal = copy.deepcopy(self.__journal)
1113
+ return newscheme
1114
+
1115
+ ###########################################################################
1116
+ def prune(self):
1117
+ '''Remove all empty parameters from configuration dictionary.
1118
+
1119
+ Also deletes 'help' and 'example' keys.
1120
+ '''
1121
+ # When at top of tree loop maxdepth times to make sure all stale
1122
+ # branches have been removed, not elegant, but stupid-simple
1123
+ # "good enough"
1124
+
1125
+ # 10 should be enough for anyone...
1126
+ maxdepth = 10
1127
+
1128
+ for _ in range(maxdepth):
1129
+ self.__prune()
1130
+
1131
+ ###########################################################################
1132
+ def __prune(self, *keypath):
1133
+ '''
1134
+ Internal recursive function that creates a local copy of the Chip
1135
+ schema (cfg) with only essential non-empty parameters retained.
1136
+
1137
+ '''
1138
+ cfg = self.__search(*keypath)
1139
+
1140
+ # Prune when the default & value are set to the following
1141
+ # Loop through all keys starting at the top
1142
+ for k in list(cfg.keys()):
1143
+ # removing all default/template keys
1144
+ # reached a default subgraph, delete it
1145
+ if k == 'default':
1146
+ del cfg[k]
1147
+ # reached leaf-cell
1148
+ elif 'help' in cfg[k].keys():
1149
+ del cfg[k]['help']
1150
+ elif 'example' in cfg[k].keys():
1151
+ del cfg[k]['example']
1152
+ elif Schema._is_leaf(cfg[k]):
1153
+ pass
1154
+ # removing stale branches
1155
+ elif not cfg[k]:
1156
+ cfg.pop(k)
1157
+ # keep traversing tree
1158
+ else:
1159
+ self.__prune(*keypath, k)
1160
+
1161
+ ###########################################################################
1162
+ def is_empty(self, *keypath):
1163
+ '''
1164
+ Utility function to check key for an empty value.
1165
+ '''
1166
+ empty = (None, [])
1167
+
1168
+ values = self._getvals(*keypath)
1169
+ defvalue = self.get_default(*keypath)
1170
+ value_empty = (defvalue in empty) and \
1171
+ all([value in empty for value, _, _ in values])
1172
+ return value_empty
1173
+
1174
+ ###########################################################################
1175
+ def history(self, job):
1176
+ '''
1177
+ Returns a *mutable* reference to ['history', job] as a Schema object.
1178
+
1179
+ If job doesn't currently exist in history, create it with default
1180
+ values.
1181
+
1182
+ Args:
1183
+ job (str): Name of historical job to return.
1184
+ '''
1185
+ if job not in self.cfg['history']:
1186
+ self.cfg['history'][job] = self._init_schema_cfg()
1187
+
1188
+ # Can't initialize Schema() by passing in cfg since it performs a deep
1189
+ # copy.
1190
+ schema = Schema()
1191
+ schema.cfg = self.cfg['history'][job]
1192
+ return schema
1193
+
1194
+ #######################################
1195
+ def _init_logger(self, parent=None):
1196
+ if parent:
1197
+ # If parent provided, create a child logger
1198
+ self.logger = parent.getChild('schema')
1199
+ else:
1200
+ # Check if the logger exists and create
1201
+ if not hasattr(self, 'logger') or not self.logger:
1202
+ self.logger = logging.getLogger(f'sc_schema_{id(self)}')
1203
+ self.logger.propagate = False
1204
+
1205
+ #######################################
1206
+ def __getstate__(self):
1207
+ attributes = self.__dict__.copy()
1208
+
1209
+ # We have to remove the chip's logger before serializing the object
1210
+ # since the logger object is not serializable.
1211
+ del attributes['logger']
1212
+ return attributes
1213
+
1214
+ #######################################
1215
+ def __setstate__(self, state):
1216
+ self.__dict__ = state
1217
+
1218
+ # Reinitialize logger on restore
1219
+ self._init_logger()
1220
+
1221
+ #######################################
1222
+ def __record_journal(self, record_type, key, value=None, field=None, step=None, index=None):
1223
+ '''
1224
+ Record the schema transaction
1225
+ '''
1226
+ if self.__journal is None:
1227
+ return
1228
+
1229
+ self.__journal.append({
1230
+ "type": record_type,
1231
+ "key": key,
1232
+ "value": value,
1233
+ "field": field,
1234
+ "step": step,
1235
+ "index": index
1236
+ })
1237
+
1238
+ #######################################
1239
+ def _start_journal(self):
1240
+ '''
1241
+ Start journaling the schema transactions
1242
+ '''
1243
+ self.__journal = []
1244
+
1245
+ #######################################
1246
+ def _stop_journal(self):
1247
+ '''
1248
+ Stop journaling the schema transactions
1249
+ '''
1250
+ self.__journal = None
1251
+
1252
+ #######################################
1253
+ def read_journal(self, filename):
1254
+ '''
1255
+ Reads a manifest and replays the journal
1256
+ '''
1257
+
1258
+ schema = Schema(logger=self.logger)
1259
+ _, schema.__journal = Schema.__read_manifest_file(str(filename))
1260
+ self._import_journal(schema)
1261
+
1262
+ #######################################
1263
+ def _import_journal(self, schema):
1264
+ '''
1265
+ Import the journaled transactions from a different schema
1266
+ '''
1267
+ if not schema.__journal:
1268
+ return
1269
+
1270
+ for action in schema.__journal:
1271
+ record_type = action['type']
1272
+ keypath = action['key']
1273
+ value = action['value']
1274
+ field = action['field']
1275
+ step = action['step']
1276
+ index = action['index']
1277
+ try:
1278
+ if record_type == 'set':
1279
+ cfg = self.__search(*keypath, insert_defaults=True)
1280
+ self.__set(*keypath, value, logger=self.logger, cfg=cfg, field=field,
1281
+ step=step, index=index, journal_callback=None)
1282
+ elif record_type == 'add':
1283
+ cfg = self.__search(*keypath, insert_defaults=True)
1284
+ self._add(*keypath, value, cfg=cfg, field=field, step=step, index=index)
1285
+ elif record_type == 'unset':
1286
+ self.unset(*keypath, step=step, index=index)
1287
+ elif record_type == 'remove':
1288
+ self.remove(*keypath)
1289
+ else:
1290
+ raise ValueError(f'Unknown record type {record_type}')
1291
+ except Exception as e:
1292
+ self.logger.error(f'Exception: {e}')
1293
+
1294
+ #######################################
1295
+ def get_default(self, *keypath):
1296
+ '''Returns default value of a parameter.
1297
+
1298
+ Args:
1299
+ keypath(list str): Variable length schema key list.
1300
+ '''
1301
+ cfg = self.__search(*keypath)
1302
+
1303
+ if not Schema._is_leaf(cfg):
1304
+ raise ValueError(f'Invalid keypath {keypath}: get_default() '
1305
+ 'must be called on a complete keypath')
1306
+
1307
+ return cfg['node']['default']['default']['value']
1308
+
1309
+ #######################################
1310
+ def set_default(self, *args):
1311
+ '''Sets the default value of a parameter.
1312
+
1313
+ Args:
1314
+ args (list str): Variable length schema key list and value.
1315
+ '''
1316
+ keypath = args[:-1]
1317
+ value = args[-1]
1318
+ cfg = self.__search(*keypath)
1319
+
1320
+ if not Schema._is_leaf(cfg):
1321
+ raise ValueError(f'Invalid keypath {keypath}: set_default() '
1322
+ 'must be called on a complete keypath')
1323
+
1324
+ allowed_values = None
1325
+ if 'enum' in cfg:
1326
+ allowed_values = cfg['enum']
1327
+
1328
+ cfg['node']['default']['default']['value'] = Schema.__check_and_normalize(
1329
+ value, cfg['type'], 'value', keypath, allowed_values)
1330
+
1331
+ ###########################################################################
1332
+ def create_cmdline(self,
1333
+ progname,
1334
+ description=None,
1335
+ switchlist=None,
1336
+ input_map=None,
1337
+ additional_args=None,
1338
+ version=None,
1339
+ print_banner=None,
1340
+ input_map_handler=None,
1341
+ preprocess_keys=None,
1342
+ post_process=None,
1343
+ logger=None):
1344
+ """Creates a Schema command line interface.
1345
+
1346
+ Exposes parameters in the SC schema as command line switches,
1347
+ simplifying creation of SC apps with a restricted set of schema
1348
+ parameters exposed at the command line. The order of command
1349
+ line switch settings parsed from the command line is as follows:
1350
+
1351
+ 1. loglevel, if available in schema
1352
+ 2. read_manifest([cfg]), if available in schema
1353
+ 3. read inputs with input_map_handler
1354
+ 4. all other switches
1355
+ 5. Run post_process
1356
+
1357
+ The cmdline interface is implemented using the Python argparse package
1358
+ and the following use restrictions apply.
1359
+
1360
+ * Help is accessed with the '-h' switch.
1361
+ * Arguments that include spaces must be enclosed with double quotes.
1362
+ * List parameters are entered individually. (ie. -y libdir1 -y libdir2)
1363
+ * For parameters with Boolean types, the switch implies "true".
1364
+ * Special characters (such as '-') must be enclosed in double quotes.
1365
+ * Compiler compatible switches include: -D, -I, -O{0,1,2,3}
1366
+ * Legacy switch formats are supported: +libext+, +incdir+
1367
+
1368
+ Args:
1369
+ progname (str): Name of program to be executed.
1370
+ description (str): Short program description.
1371
+ switchlist (list of str): List of SC parameter switches to expose
1372
+ at the command line. By default all SC schema switches are
1373
+ available. Parameter switches should be entered based on the
1374
+ parameter 'switch' field in the schema. For parameters with
1375
+ multiple switches, both will be accepted if any one is included
1376
+ in this list.
1377
+ input_map (dict of str): Dictionary mapping file extensions to input
1378
+ filetypes. This is used to automatically assign positional
1379
+ source arguments to ['input', 'fileset', ...] keypaths based on their file
1380
+ extension. If None, the CLI will not accept positional source
1381
+ arguments.
1382
+ additional_args (dict of dict): Dictionary of extra arguments to add
1383
+ to the command line parser, with the arguments matching the
1384
+ argparse.add_argument() call.
1385
+ version (str): Version to report when calling with -version
1386
+ print_banner (function): Function callback to print command line banner
1387
+ input_map_handler (function): Function callback handle inputs to the input map
1388
+ preprocess_keys (function): Function callback to preprocess keys that need to be
1389
+ corrected
1390
+ post_process (function): Function callback to process arguments before returning
1391
+
1392
+ Returns:
1393
+ None if additional_args is not provided, otherwise a dictionary with the
1394
+ command line options detected from the additional_args
1395
+
1396
+ Examples:
1397
+ >>> schema.create_cmdline(progname='sc-show',switchlist=['-input','-cfg'])
1398
+ Creates a command line interface for 'sc-show' app.
1399
+ >>> schema.create_cmdline(progname='sc', input_map={'v': ('rtl', 'verilog')})
1400
+ All sources ending in .v will be stored in ['input', 'rtl', 'verilog']
1401
+ >>> extra = schema.create_cmdline(progname='sc',
1402
+ additional_args={'-demo': {'action': 'store_true'}})
1403
+ Returns extra = {'demo': False/True}
1404
+ """
1405
+
1406
+ if not logger:
1407
+ logger = self.logger
1408
+
1409
+ # Argparse
1410
+ parser = argparse.ArgumentParser(prog=progname,
1411
+ prefix_chars='-+',
1412
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1413
+ description=description,
1414
+ allow_abbrev=False)
1415
+
1416
+ # Get a new schema, in case values have already been set
1417
+ schema_class = type(self)
1418
+ schema = schema_class(logger=self.logger)
1419
+
1420
+ # Iterate over all keys from an empty schema to add parser arguments
1421
+ used_switches = set()
1422
+ for keypath in schema.allkeys():
1423
+ # Fetch fields from leaf cell
1424
+ helpstr = schema.get(*keypath, field='shorthelp')
1425
+ typestr = schema.get(*keypath, field='type')
1426
+ pernodestr = schema.get(*keypath, field='pernode')
1427
+
1428
+ # argparse 'dest' must be a string, so join keypath with commas
1429
+ dest = '_'.join(keypath)
1430
+
1431
+ switchstrs, metavar = self.__get_switches(schema, *keypath)
1432
+
1433
+ # Three switch types (bool, list, scalar)
1434
+ if not switchlist or any(switch in switchlist for switch in switchstrs):
1435
+ used_switches.update(switchstrs)
1436
+ if typestr == 'bool':
1437
+ # Boolean type arguments
1438
+ if pernodestr == 'never':
1439
+ parser.add_argument(*switchstrs,
1440
+ nargs='?',
1441
+ metavar=metavar,
1442
+ dest=dest,
1443
+ const='true',
1444
+ help=helpstr,
1445
+ default=argparse.SUPPRESS)
1446
+ else:
1447
+ parser.add_argument(*switchstrs,
1448
+ metavar=metavar,
1449
+ nargs='?',
1450
+ dest=dest,
1451
+ action='append',
1452
+ help=helpstr,
1453
+ default=argparse.SUPPRESS)
1454
+ elif '[' in typestr or pernodestr != 'never':
1455
+ # list type arguments
1456
+ parser.add_argument(*switchstrs,
1457
+ metavar=metavar,
1458
+ dest=dest,
1459
+ action='append',
1460
+ help=helpstr,
1461
+ default=argparse.SUPPRESS)
1462
+ else:
1463
+ # all the rest
1464
+ parser.add_argument(*switchstrs,
1465
+ metavar=metavar,
1466
+ dest=dest,
1467
+ help=helpstr,
1468
+ default=argparse.SUPPRESS)
1469
+
1470
+ # Check if there are invalid switches
1471
+ if switchlist:
1472
+ for switch in switchlist:
1473
+ if switch not in used_switches:
1474
+ raise ValueError(f'{switch} is not a valid commandline argument')
1475
+
1476
+ if input_map is not None and input_map_handler:
1477
+ parser.add_argument('source',
1478
+ nargs='*',
1479
+ help='Input files with filetype inferred by extension')
1480
+
1481
+ # Preprocess sys.argv to enable linux commandline switch formats
1482
+ # (gcc, verilator, etc)
1483
+ scargs = []
1484
+
1485
+ # Iterate from index 1, otherwise we end up with script name as a
1486
+ # 'source' positional argument
1487
+ for argument in sys.argv[1:]:
1488
+ # Split switches with one character and a number after (O0,O1,O2)
1489
+ opt = re.match(r'(\-\w)(\d+)', argument)
1490
+ # Split assign switches (-DCFG_ASIC=1)
1491
+ assign = re.search(r'(\-\w)(\w+\=\w+)', argument)
1492
+ # Split plusargs (+incdir+/path)
1493
+ plusarg = re.search(r'(\+\w+\+)(.*)', argument)
1494
+ if opt:
1495
+ scargs.append(opt.group(1))
1496
+ scargs.append(opt.group(2))
1497
+ elif plusarg:
1498
+ scargs.append(plusarg.group(1))
1499
+ scargs.append(plusarg.group(2))
1500
+ elif assign:
1501
+ scargs.append(assign.group(1))
1502
+ scargs.append(assign.group(2))
1503
+ else:
1504
+ scargs.append(argument)
1505
+
1506
+ if version:
1507
+ parser.add_argument('-version', action='version', version=version)
1508
+
1509
+ print_additional_arg_value = {}
1510
+ if additional_args:
1511
+ # Add additional user specified arguments
1512
+ arg_dests = []
1513
+ for arg, arg_detail in additional_args.items():
1514
+ do_print = True
1515
+ if "sc_print" in arg_detail:
1516
+ do_print = arg_detail["sc_print"]
1517
+ del arg_detail["sc_print"]
1518
+ argument = parser.add_argument(arg, **arg_detail)
1519
+ print_additional_arg_value[argument.dest] = do_print
1520
+
1521
+ arg_dests.append(argument.dest)
1522
+ # rewrite additional_args with new dest information
1523
+ additional_args = arg_dests
1524
+
1525
+ # Grab argument from pre-process sysargs
1526
+ cmdargs = vars(parser.parse_args(scargs))
1527
+
1528
+ if print_banner:
1529
+ print_banner()
1530
+
1531
+ extra_params = None
1532
+ if additional_args:
1533
+ # Grab user specified arguments
1534
+ extra_params = {}
1535
+ for arg in additional_args:
1536
+ if arg in cmdargs:
1537
+ val = cmdargs[arg]
1538
+ if print_additional_arg_value[arg]:
1539
+ msg = f'Command line argument entered: "{arg}" Value: {val}'
1540
+ self.logger.info(msg)
1541
+ extra_params[arg] = val
1542
+ # Remove from cmdargs
1543
+ del cmdargs[arg]
1544
+
1545
+ # Set loglevel if set at command line
1546
+ if 'option_loglevel' in cmdargs.keys():
1547
+ log_level = cmdargs['option_loglevel']
1548
+ if isinstance(log_level, list):
1549
+ # if multiple found, pick the first one
1550
+ log_level = log_level[0]
1551
+ logger.setLevel(translate_loglevel(log_level).split()[-1])
1552
+
1553
+ # Read in all cfg files
1554
+ if 'option_cfg' in cmdargs.keys():
1555
+ for item in cmdargs['option_cfg']:
1556
+ self.read_manifest(item, clobber=True, clear=True, allow_missing_keys=True)
1557
+
1558
+ if input_map_handler:
1559
+ # Map sources to ['input'] keypath.
1560
+ if 'source' in cmdargs:
1561
+ input_map_handler(cmdargs['source'])
1562
+ # we don't want to handle this in the next loop
1563
+ del cmdargs['source']
1564
+
1565
+ # Cycle through all command args and write to manifest
1566
+ for dest, vals in cmdargs.items():
1567
+ keypath = dest.split('_')
1568
+
1569
+ # Turn everything into a list for uniformity
1570
+ if not isinstance(vals, list):
1571
+ vals = [vals]
1572
+
1573
+ # Cycle through all items
1574
+ for item in vals:
1575
+ if item is None:
1576
+ # nargs=? leaves a None for booleans
1577
+ item = ''
1578
+
1579
+ if preprocess_keys:
1580
+ item = preprocess_keys(keypath, item)
1581
+
1582
+ num_free_keys = keypath.count('default')
1583
+
1584
+ switches, metavar = self.__get_switches(schema, *keypath)
1585
+ switchstr = '/'.join(switches)
1586
+
1587
+ if len(item.split(' ')) < num_free_keys + 1:
1588
+ # Error out if value provided doesn't have enough words to
1589
+ # fill in 'default' keys.
1590
+ raise ValueError(f'Invalid value {item} for switch {switchstr}. '
1591
+ f'Expected format {metavar}.')
1592
+
1593
+ # We replace 'default' in keypath with first N words in provided
1594
+ # value.
1595
+ *free_keys, remainder = item.split(' ', num_free_keys)
1596
+ args = [free_keys.pop(0) if key == 'default' else key for key in keypath]
1597
+
1598
+ # Remainder is the value we want to set, possibly with a step/index value beforehand
1599
+ sctype = self.get(*keypath, field='type')
1600
+ pernode = self.get(*keypath, field='pernode')
1601
+ step, index = None, None
1602
+ if pernode == 'required':
1603
+ try:
1604
+ step, index, val = remainder.split(' ', 2)
1605
+ except ValueError:
1606
+ self.logger.error(f"Invalid value '{item}' for switch {switchstr}. "
1607
+ "Requires step and index before final value.")
1608
+ elif pernode == 'optional':
1609
+ # Split on spaces, preserving items that are grouped in quotes
1610
+ items = shlex.split(remainder)
1611
+ if len(items) > 3:
1612
+ self.logger.error(f"Invalid value '{item}'' for switch {switchstr}. "
1613
+ "Too many arguments, please wrap multiline "
1614
+ "strings in quotes.")
1615
+ continue
1616
+ if sctype == 'bool':
1617
+ if len(items) == 3:
1618
+ step, index, val = items
1619
+ elif len(items) == 2:
1620
+ step, val = items
1621
+ if val != 'true' and val != 'false':
1622
+ index = val
1623
+ val = True
1624
+ elif len(items) == 1:
1625
+ val, = items
1626
+ if val != 'true' and val != 'false':
1627
+ step = val
1628
+ val = True
1629
+ else:
1630
+ val = True
1631
+ else:
1632
+ if len(items) == 3:
1633
+ step, index, val = items
1634
+ elif len(items) == 2:
1635
+ step, val = items
1636
+ else:
1637
+ val, = items
1638
+ else:
1639
+ val = remainder
1640
+
1641
+ msg = f'Command line argument entered: {args} Value: {val}'
1642
+ if step is not None:
1643
+ msg += f' Step: {step}'
1644
+ if index is not None:
1645
+ msg += f' Index: {index}'
1646
+ self.logger.info(msg)
1647
+
1648
+ # Storing in manifest
1649
+ typestr = schema.get(*keypath, field='type')
1650
+ if typestr.startswith('['):
1651
+ if self.valid(*args):
1652
+ self.add(*args, val, step=step, index=index)
1653
+ else:
1654
+ self.set(*args, val, step=step, index=index, clobber=True)
1655
+ else:
1656
+ self.set(*args, val, step=step, index=index, clobber=True)
1657
+
1658
+ if post_process:
1659
+ post_process(cmdargs)
1660
+
1661
+ return extra_params
1662
+
1663
+ ###########################################################################
1664
+ def __get_switches(self, schema, *keypath):
1665
+ '''Helper function for parsing switches and metavars for a keypath.'''
1666
+ # Switch field fully describes switch format
1667
+ switch = schema.get(*keypath, field='switch')
1668
+
1669
+ if switch is None:
1670
+ switches = []
1671
+ elif isinstance(switch, list):
1672
+ switches = switch
1673
+ else:
1674
+ switches = [switch]
1675
+ switchstrs = []
1676
+
1677
+ # parse out switch from metavar
1678
+ # TODO: should we validate that metavar matches for each switch?
1679
+ for switch in switches:
1680
+ switchmatch = re.match(r'(-[\w_]+)\s+(.*)', switch)
1681
+ gccmatch = re.match(r'(-[\w_]+)(.*)', switch)
1682
+ plusmatch = re.match(r'(\+[\w_\+]+)(.*)', switch)
1683
+
1684
+ if switchmatch:
1685
+ switchstr = switchmatch.group(1)
1686
+ metavar = switchmatch.group(2)
1687
+ elif gccmatch:
1688
+ switchstr = gccmatch.group(1)
1689
+ metavar = gccmatch.group(2)
1690
+ elif plusmatch:
1691
+ switchstr = plusmatch.group(1)
1692
+ metavar = plusmatch.group(2)
1693
+ switchstrs.append(switchstr)
1694
+
1695
+ return switchstrs, metavar
1696
+
1697
+ ###########################################################################
1698
+ def read_manifest(self, filename, clear=True, clobber=True, allow_missing_keys=True):
1699
+ """
1700
+ Reads a manifest from disk and merges it with the current manifest.
1701
+
1702
+ The file format read is determined by the filename suffix. Currently
1703
+ json (*.json) and yaml(*.yaml) formats are supported.
1704
+
1705
+ Args:
1706
+ filename (filepath): Path to a manifest file to be loaded.
1707
+ clear (bool): If True, disables append operations for list type.
1708
+ clobber (bool): If True, overwrites existing parameter value.
1709
+ allow_missing_keys (bool): If True, keys not present in current schema will be ignored.
1710
+
1711
+ Examples:
1712
+ >>> chip.read_manifest('mychip.json')
1713
+ Loads the file mychip.json into the current Chip object.
1714
+ """
1715
+ schema = Schema(manifest=filename, logger=self.logger)
1716
+
1717
+ if schema.get('schemaversion') != self.get('schemaversion'):
1718
+ self.logger.warning("Mismatch in schema versions: "
1719
+ f"{schema.get('schemaversion')} != {self.get('schemaversion')}")
1720
+
1721
+ for keylist in schema.allkeys():
1722
+ if keylist[0] in ('history', 'library'):
1723
+ continue
1724
+ if 'default' in keylist:
1725
+ continue
1726
+ typestr = schema.get(*keylist, field='type')
1727
+ should_append = '[' in typestr and not clear
1728
+
1729
+ if allow_missing_keys and not self.valid(*keylist, default_valid=True):
1730
+ self.logger.warning(f'{keylist} not found in schema, skipping...')
1731
+ continue
1732
+
1733
+ for val, step, index in schema._getvals(*keylist, return_defvalue=False):
1734
+ # update value, handling scalars vs. lists
1735
+ if should_append:
1736
+ self.add(*keylist, val, step=step, index=index)
1737
+ else:
1738
+ self.set(*keylist, val, step=step, index=index, clobber=clobber)
1739
+
1740
+ # update other pernode fields
1741
+ # TODO: only update these if clobber is successful
1742
+ step_key = Schema.GLOBAL_KEY if not step else step
1743
+ idx_key = Schema.GLOBAL_KEY if not index else index
1744
+ for field in schema.getdict(*keylist)['node'][step_key][idx_key].keys():
1745
+ if field == 'value':
1746
+ continue
1747
+ v = schema.get(*keylist, step=step, index=index, field=field)
1748
+ if should_append:
1749
+ self.add(*keylist, v, step=step, index=index, field=field)
1750
+ else:
1751
+ self.set(*keylist, v, step=step, index=index, field=field)
1752
+
1753
+ # update other fields that a user might modify
1754
+ for field in schema.getdict(*keylist).keys():
1755
+ if field in ('node',):
1756
+ # skip these fields (node handled above)
1757
+ continue
1758
+
1759
+ # TODO: should we be taking into consideration clobber for these fields?
1760
+ v = schema.get(*keylist, field=field)
1761
+ self.set(*keylist, v, field=field)
1762
+
1763
+ # Read history, if we're not already reading into a job
1764
+ if 'history' in schema.getkeys():
1765
+ for historic_job in schema.getkeys('history'):
1766
+ self.cfg['history'][historic_job] = schema.getdict('history', historic_job)
1767
+
1768
+ # TODO: better way to handle this?
1769
+ if 'library' in schema.getkeys():
1770
+ for libname in schema.getkeys('library'):
1771
+ self.cfg['library'][libname] = schema.getdict('library', libname)
1772
+
1773
+ ###########################################################################
1774
+ def merge_manifest(self, src, job=None, clobber=True, clear=True, check=False):
1775
+ """
1776
+ Merges a given manifest with the current compilation manifest.
1777
+
1778
+ All value fields in the provided schema dictionary are merged into the
1779
+ current chip object. Dictionaries with non-existent keypath produces a
1780
+ logger error message and raises the Chip object error flag.
1781
+
1782
+ Args:
1783
+ src (Schema): Schema object to merge
1784
+ job (str): Specifies non-default job to merge into
1785
+ clear (bool): If True, disables append operations for list type
1786
+ clobber (bool): If True, overwrites existing parameter value
1787
+ check (bool): If True, checks the validity of each key
1788
+ """
1789
+ if job is not None:
1790
+ dest = self.history(job)
1791
+ else:
1792
+ dest = self
1793
+
1794
+ for keylist in src.allkeys():
1795
+ if keylist[0] in ('history', 'library'):
1796
+ continue
1797
+ # only read in valid keypaths without 'default'
1798
+ key_valid = True
1799
+ if check:
1800
+ key_valid = dest.valid(*keylist, default_valid=True)
1801
+ if not key_valid:
1802
+ self.logger.warning(f'Keypath {keylist} is not valid')
1803
+ if key_valid and 'default' not in keylist:
1804
+ typestr = src.get(*keylist, field='type')
1805
+ should_append = '[' in typestr and not clear
1806
+ key_cfg = src.__search(*keylist)
1807
+ for val, step, index in src._getvals(*keylist, return_defvalue=False):
1808
+ # update value, handling scalars vs. lists
1809
+ if should_append:
1810
+ dest.add(*keylist, val, step=step, index=index)
1811
+ else:
1812
+ dest.set(*keylist, val, step=step, index=index, clobber=clobber)
1813
+
1814
+ # update other pernode fields
1815
+ # TODO: only update these if clobber is successful
1816
+ step_key = Schema.GLOBAL_KEY if not step else step
1817
+ idx_key = Schema.GLOBAL_KEY if not index else index
1818
+ for field in key_cfg['node'][step_key][idx_key].keys():
1819
+ if field == 'value':
1820
+ continue
1821
+ v = src.get(*keylist, step=step, index=index, field=field)
1822
+ if should_append:
1823
+ dest.add(*keylist, v, step=step, index=index, field=field)
1824
+ else:
1825
+ dest.set(*keylist, v, step=step, index=index, field=field)
1826
+
1827
+ # update other fields that a user might modify
1828
+ for field in key_cfg.keys():
1829
+ if field in ('node', 'switch', 'type', 'require',
1830
+ 'shorthelp', 'example', 'help'):
1831
+ # skip these fields (node handled above, others are static)
1832
+ continue
1833
+ # TODO: should we be taking into consideration clobber for these fields?
1834
+ v = src.get(*keylist, field=field)
1835
+ dest.set(*keylist, v, field=field)
1836
+
1837
+
1838
+ if _has_yaml:
1839
+ class YamlIndentDumper(yaml.Dumper):
1840
+ def increase_indent(self, flow=False, indentless=False):
1841
+ return super(YamlIndentDumper, self).increase_indent(flow, False)