opencos-eda 0.2.49__tar.gz → 0.2.51__tar.gz

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 (97) hide show
  1. {opencos_eda-0.2.49/opencos_eda.egg-info → opencos_eda-0.2.51}/PKG-INFO +1 -1
  2. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/lec.py +7 -4
  3. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/multi.py +1 -1
  4. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/shell.py +11 -8
  5. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/sim.py +43 -22
  6. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/deps/deps_file.py +67 -14
  7. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda.py +15 -3
  8. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_base.py +54 -20
  9. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_config.py +6 -0
  10. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_config_defaults.yml +36 -14
  11. opencos_eda-0.2.51/opencos/eda_deps_sanitize.py +73 -0
  12. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_eda_elab.py +2 -1
  13. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_tools.py +1 -0
  14. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/modelsim_ase.py +22 -0
  15. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/questa.py +5 -3
  16. opencos_eda-0.2.51/opencos/tools/questa_fse.py +59 -0
  17. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/slang.py +8 -2
  18. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/verilator.py +25 -0
  19. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/vivado.py +33 -26
  20. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/yosys.py +21 -5
  21. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/util.py +183 -8
  22. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/utils/markup_helpers.py +31 -2
  23. opencos_eda-0.2.51/opencos/utils/status_constants.py +27 -0
  24. opencos_eda-0.2.51/opencos/utils/vsim_helper.py +55 -0
  25. {opencos_eda-0.2.49 → opencos_eda-0.2.51/opencos_eda.egg-info}/PKG-INFO +1 -1
  26. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos_eda.egg-info/SOURCES.txt +4 -0
  27. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos_eda.egg-info/entry_points.txt +1 -0
  28. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/pyproject.toml +3 -1
  29. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/LICENSE +0 -0
  30. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/LICENSE.spdx +0 -0
  31. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/README.md +0 -0
  32. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/__init__.py +0 -0
  33. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/_version.py +0 -0
  34. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/_waves_pkg.sv +0 -0
  35. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/__init__.py +0 -0
  36. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/build.py +0 -0
  37. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/elab.py +0 -0
  38. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/export.py +0 -0
  39. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/flist.py +0 -0
  40. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/open.py +0 -0
  41. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/proj.py +0 -0
  42. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/sweep.py +0 -0
  43. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/synth.py +0 -0
  44. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/targets.py +0 -0
  45. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/upload.py +0 -0
  46. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/commands/waves.py +0 -0
  47. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/deps/__init__.py +0 -0
  48. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/deps/defaults.py +0 -0
  49. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/deps/deps_commands.py +0 -0
  50. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/deps/deps_processor.py +0 -0
  51. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/deps_schema.py +0 -0
  52. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_config_max_verilator_waivers.yml +0 -0
  53. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_config_reduced.yml +0 -0
  54. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_deps_bash_completion.bash +0 -0
  55. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_extract_targets.py +0 -0
  56. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/eda_tool_helper.py +0 -0
  57. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/export_helper.py +0 -0
  58. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/export_json_convert.py +0 -0
  59. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/files.py +0 -0
  60. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/hw/__init__.py +0 -0
  61. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/hw/oc_cli.py +0 -0
  62. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/hw/pcie.py +0 -0
  63. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/names.py +0 -0
  64. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/peakrdl_cleanup.py +0 -0
  65. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/seed.py +0 -0
  66. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/__init__.py +0 -0
  67. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/custom_config.yml +0 -0
  68. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/deps_files/command_order/DEPS.yml +0 -0
  69. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/deps_files/error_msgs/DEPS.yml +0 -0
  70. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -0
  71. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  72. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -0
  73. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -0
  74. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -0
  75. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/helpers.py +0 -0
  76. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_build.py +0 -0
  77. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_deps_helpers.py +0 -0
  78. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_deps_schema.py +0 -0
  79. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_eda.py +0 -0
  80. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_eda_synth.py +0 -0
  81. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tests/test_oc_cli.py +0 -0
  82. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/__init__.py +0 -0
  83. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/invio.py +0 -0
  84. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/invio_helpers.py +0 -0
  85. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/invio_yosys.py +0 -0
  86. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/iverilog.py +0 -0
  87. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/riviera.py +0 -0
  88. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/slang_yosys.py +0 -0
  89. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/surelog.py +0 -0
  90. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/tools/tabbycad_yosys.py +0 -0
  91. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/utils/__init__.py +0 -0
  92. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/utils/str_helpers.py +0 -0
  93. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos/utils/subprocess_helpers.py +0 -0
  94. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos_eda.egg-info/dependency_links.txt +0 -0
  95. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos_eda.egg-info/requires.txt +0 -0
  96. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/opencos_eda.egg-info/top_level.txt +0 -0
  97. {opencos_eda-0.2.49 → opencos_eda-0.2.51}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.49
3
+ Version: 0.2.51
4
4
  Summary: A simple Python package for wrapping RTL simuliatons and synthesis
5
5
  Author-email: Simon Sabato <simon@cognichip.ai>, Drew Ranck <drew@cognichip.ai>
6
6
  Project-URL: Homepage, https://github.com/cognichip/opencos
@@ -69,16 +69,13 @@ class CommandLec(CommandDesign):
69
69
  if self.status_any_error():
70
70
  return unparsed
71
71
 
72
- # add defines for this job type
73
- if not self.args['top']:
74
- self.args['top'] = 'eda.lec'
75
-
76
72
  # we require there to be two --designs set.
77
73
  if not self.args['designs'] and len(self.args['designs']) != 2:
78
74
  self.error('Requires two designs via --designs=<target1> --designs=<target2>',
79
75
  f'designs={self.args["designs"]}')
80
76
  return []
81
77
 
78
+
82
79
  # Before we do anything else, make sure the two designs actually exist.
83
80
  for design in self.args['designs']:
84
81
  # maybe it's a file?
@@ -89,6 +86,12 @@ class CommandLec(CommandDesign):
89
86
  else:
90
87
  self.error(f'--designs={design}, value is not a file or target')
91
88
 
89
+ if not self.args['top']:
90
+ # Correct the 'top' name so it's eda.<target1>.<target2>.lec (shortnames)
91
+ _, short_target1 = os.path.split(self.args['designs'][0])
92
+ _, short_target2 = os.path.split(self.args['designs'][1])
93
+ self.args['top'] = f'eda.{short_target1}.{short_target2}'
94
+
92
95
  # create our work dir
93
96
  self.create_work_dir()
94
97
  self.run_dep_commands()
@@ -590,7 +590,7 @@ class CommandToolsMulti(CommandMulti):
590
590
  def multi_which_tools(self, command):
591
591
  '''Overrides CommandMulti.multi_which_tool(command), return a list of all
592
592
  possible tools that can run this command'''
593
- if self.tools is None or len(self.tools) == 0:
593
+ if self.tools is None or not self.tools:
594
594
  # wasn't set via arg --tools, so use all if possible for this command.
595
595
  which_tools = self.all_handler_commands.get(command, [])
596
596
  else:
@@ -16,6 +16,8 @@ import os
16
16
  from opencos import util, export_helper
17
17
  from opencos.eda_base import CommandDesign
18
18
 
19
+ from opencos.utils import status_constants
20
+
19
21
 
20
22
  class CommandShell(CommandDesign):
21
23
  '''Base class command handler for: eda sim ...'''
@@ -181,7 +183,6 @@ class CommandShell(CommandDesign):
181
183
  _must_strings.append(self.args['pass-pattern'])
182
184
 
183
185
  if len(_bad_strings) > 0 or len(_must_strings) > 0:
184
- hit_bad_string = False
185
186
  hit_must_string_dict = dict.fromkeys(_must_strings)
186
187
  fname = os.path.join(self.args['work-dir'], filename)
187
188
  with open(fname, 'r', encoding='utf-8') as f:
@@ -191,12 +192,14 @@ class CommandShell(CommandDesign):
191
192
  if k in line:
192
193
  hit_must_string_dict[k] = True
193
194
  if any(bad_str in line for bad_str in _bad_strings):
194
- hit_bad_string = True
195
- self.error(f"log {fname}:{lineno} contains one of {_bad_strings=}")
195
+ self.error(
196
+ f"log {fname}:{lineno} contains one of {_bad_strings=}",
197
+ error_code=status_constants.EDA_SHELL_LOG_HAS_BAD_STRING
198
+ )
196
199
 
197
- if hit_bad_string:
198
- self.status += 1
199
200
  if any(x is None for x in hit_must_string_dict.values()):
200
- self.error(f"Didn't get all passing patterns in log {fname}: {_must_strings=}",
201
- f" {hit_must_string_dict=}")
202
- self.status += 1
201
+ self.error(
202
+ f"Didn't get all passing patterns in log {fname}: {_must_strings=}",
203
+ f" {hit_must_string_dict=}",
204
+ error_code=status_constants.EDA_SHELL_LOG_MISSING_MUST_STRING
205
+ )
@@ -15,6 +15,7 @@ import os
15
15
 
16
16
  from opencos import util, export_helper
17
17
  from opencos.eda_base import CommandDesign, Tool
18
+ from opencos.utils import status_constants
18
19
 
19
20
  class CommandSim(CommandDesign):
20
21
  '''Base class command handler for: eda sim ...'''
@@ -170,13 +171,21 @@ class CommandSim(CommandDesign):
170
171
  if log_filename:
171
172
  log_fname = log_filename
172
173
 
173
- self.exec(work_dir=self.args['work-dir'], command_list=clist, tee_fpath=tee_fpath)
174
+ _, stdout, _ = self.exec(
175
+ work_dir=self.args['work-dir'], command_list=clist, tee_fpath=tee_fpath
176
+ )
174
177
 
175
178
  if check_logs and log_fname:
176
179
  self.check_logs_for_errors(
177
- filename=log_fname, bad_strings=bad_strings, must_strings=must_strings,
180
+ filename='', file_contents_str=stdout,
181
+ bad_strings=bad_strings, must_strings=must_strings,
178
182
  use_bad_strings=use_bad_strings, use_must_strings=use_must_strings
179
183
  )
184
+ if log_fname:
185
+ self.artifacts_add(
186
+ name=os.path.join(self.args['work-dir'], log_fname),
187
+ typ='text', description='Simulator stdout/stderr log file'
188
+ )
180
189
 
181
190
  def do_export(self) -> None:
182
191
  '''CommandSim helper for handling args --export*
@@ -268,8 +277,8 @@ class CommandSim(CommandDesign):
268
277
  '''
269
278
  return
270
279
 
271
- def check_logs_for_errors( # pylint: disable=dangerous-default-value
272
- self, filename: str,
280
+ def check_logs_for_errors( # pylint: disable=dangerous-default-value,too-many-locals,too-many-branches
281
+ self, filename: str = '', file_contents_str: str = '',
273
282
  bad_strings: list = [], must_strings: list = [],
274
283
  use_bad_strings: bool = True, use_must_strings: bool = True
275
284
  ) -> None:
@@ -289,26 +298,38 @@ class CommandSim(CommandDesign):
289
298
  if self.args['pass-pattern'] != "":
290
299
  _must_strings.append(self.args['pass-pattern'])
291
300
 
292
- if len(_bad_strings) > 0 or len(_must_strings) > 0:
293
- hit_bad_string = False
294
- hit_must_string_dict = dict.fromkeys(_must_strings)
301
+ if len(_bad_strings) == 0 and len(_must_strings) == 0:
302
+ return
303
+
304
+ hit_must_string_dict = dict.fromkeys(_must_strings)
305
+
306
+ lines = []
307
+ fname = ''
308
+ if file_contents_str:
309
+ lines = file_contents_str.split('\n')
310
+ elif filename:
295
311
  fname = os.path.join(self.args['work-dir'], filename)
296
312
  with open(fname, 'r', encoding='utf-8') as f:
297
- for lineno, line in enumerate(f):
298
- if any(must_str in line for must_str in _must_strings):
299
- for k, _ in hit_must_string_dict.items():
300
- if k in line:
301
- hit_must_string_dict[k] = True
302
- if any(bad_str in line for bad_str in _bad_strings):
303
- hit_bad_string = True
304
- self.error(f"log {fname}:{lineno} contains one of {_bad_strings=}")
305
-
306
- if hit_bad_string:
307
- self.status += 1
308
- if any(x is None for x in hit_must_string_dict.values()):
309
- self.error(f"Didn't get all passing patterns in log {fname}: {_must_strings=}",
310
- f" {hit_must_string_dict=}")
311
- self.status += 1
313
+ lines = f.readl.splitlines()
314
+
315
+ if lines:
316
+ for lineno, line in enumerate(lines):
317
+ if any(must_str in line for must_str in _must_strings):
318
+ for k, _ in hit_must_string_dict.items():
319
+ if k in line:
320
+ hit_must_string_dict[k] = True
321
+ if any(bad_str in line for bad_str in _bad_strings):
322
+ self.error(
323
+ f"log {fname}:{lineno} contains one of {_bad_strings=}",
324
+ error_code=status_constants.EDA_SIM_LOG_HAS_BAD_STRING
325
+ )
326
+
327
+ if any(x is None for x in hit_must_string_dict.values()):
328
+ self.error(
329
+ f"Didn't get all passing patterns in log {fname}: {_must_strings=}",
330
+ f" {hit_must_string_dict=}",
331
+ error_code=status_constants.EDA_SIM_LOG_MISSING_MUST_STRING
332
+ )
312
333
 
313
334
  # Methods that derived classes must override:
314
335
 
@@ -10,15 +10,20 @@ from pathlib import Path
10
10
 
11
11
  import toml
12
12
 
13
+ from opencos import util
13
14
  from opencos.deps.defaults import DEPS_FILE_EXTS, ROOT_TABLE_KEYS_NOT_TARGETS
14
15
  from opencos.util import debug, error
15
- from opencos.utils.markup_helpers import yaml_safe_load, toml_load_only_root_line_numbers
16
+ from opencos.utils.markup_helpers import yaml_safe_load, toml_load_only_root_line_numbers, \
17
+ markup_writer, markup_dumper
16
18
  from opencos.utils.str_helpers import fnmatch_or_re, dep_str2list
17
19
  from opencos.utils.subprocess_helpers import subprocess_run_background
20
+ from opencos.utils.status_constants import EDA_DEPS_FILE_NOT_FOUND, EDA_DEPS_TARGET_NOT_FOUND
18
21
 
19
22
 
20
23
  def deps_data_get_all_targets(data: dict) -> list:
21
24
  '''Given extracted DEPS data (dict) get all the root level keys that aren't defaults'''
25
+ if data is None:
26
+ return []
22
27
  return [x for x in data.keys() if x not in ROOT_TABLE_KEYS_NOT_TARGETS]
23
28
 
24
29
 
@@ -166,32 +171,43 @@ def deps_target_get_deps_list(
166
171
  ret = []
167
172
  for dep in deps:
168
173
  if isinstance(dep, str):
169
- if dep.startswith('#') or dep == '':
174
+ if not dep or dep.startswith('#'):
170
175
  continue
171
176
  ret.append(dep)
172
177
  return ret
173
178
 
174
179
 
175
180
  def deps_list_target_sanitize(
176
- entry, default_key: str = 'deps', target_node: str = '', deps_file: str = ''
181
+ entry, default_key: str = 'deps', target_node: str = '', deps_file: str = '',
182
+ entry_fix_deps_key: bool = True
177
183
  ) -> dict:
178
184
  '''Returns a sanitized DEPS markup table entry (dict --> dict)
179
185
 
180
186
  Since we support target entries that can be dict, list, or str(), sanitize
181
187
  them so they are a dict, with a key named 'deps' that has a list of deps.
182
188
  '''
189
+ ret = None
183
190
  if isinstance(entry, dict):
184
- return entry
191
+ ret = entry
185
192
 
186
193
  if isinstance(entry, str):
187
194
  mylist = dep_str2list(entry) # convert str to list
188
- return {default_key: mylist}
195
+ ret = {default_key: mylist}
189
196
 
190
197
  if isinstance(entry, list):
191
198
  # it's already a list
192
- return {default_key: entry}
199
+ ret = {default_key: entry}
200
+
201
+ if ret is not None:
202
+ if entry_fix_deps_key and 'deps' in ret:
203
+ ret['deps'] = deps_target_get_deps_list(
204
+ entry=ret, default_key='deps', deps_file=deps_file,
205
+ entry_must_have_default_key=True
206
+ )
207
+ else:
208
+ assert False, f"Can't convert to list {entry=} {default_key=} {target_node=} {deps_file=}"
193
209
 
194
- assert False, f"Can't convert to list {entry=} {default_key=} {target_node=} {deps_file=}"
210
+ return ret
195
211
 
196
212
 
197
213
  class DepsFile:
@@ -230,7 +246,10 @@ class DepsFile:
230
246
  if deps_path and os.path.exists(deps_path):
231
247
  self.rel_deps_file = os.path.join(os.path.relpath(deps_path), deps_leaf)
232
248
 
233
- self.error = command_design_ref.error # method.
249
+ self.error = getattr(command_design_ref, 'error', None)
250
+ if not self.error:
251
+ self.error = util.error
252
+
234
253
 
235
254
  def found(self) -> bool:
236
255
  '''Returns true if this DEPS file exists and extracted non-empty data'''
@@ -270,15 +289,18 @@ class DepsFile:
270
289
  if '.' in target_node:
271
290
  # Likely a filename (target_node does not include path)
272
291
  self.error(f'Trying to resolve command-line target={t_full} (file?):',
273
- f'File={t_node} not found in directory={t_path}')
292
+ f'File={t_node} not found in directory={t_path}',
293
+ error_code=EDA_DEPS_FILE_NOT_FOUND)
274
294
  elif not self.rel_deps_file:
275
295
  # target, but there's no DEPS file
276
296
  self.error(f'Trying to resolve command-line target={t_full}:',
277
- f'but path {t_path} has no DEPS markup file (DEPS.yml)')
297
+ f'but path {t_path} has no DEPS markup file (DEPS.yml)',
298
+ error_code=EDA_DEPS_FILE_NOT_FOUND)
278
299
  else:
279
300
  self.error(f'Trying to resolve command-line target={t_full}:',
280
301
  f'was not found in deps_file={self.rel_deps_file},',
281
- f'possible targets in deps file = {list(self.data.keys())}')
302
+ f'possible targets in deps file = {list(self.data.keys())}',
303
+ error_code=EDA_DEPS_TARGET_NOT_FOUND)
282
304
  else:
283
305
  # If we have caller_info, then this was a recursive call from another
284
306
  # DEPS file. It should already have the useful error messaging:
@@ -287,16 +309,19 @@ class DepsFile:
287
309
  # Likely a filename (target_node does not include path)
288
310
  self.error(f'Trying to resolve target={t_full} (file?):',
289
311
  f'called from {caller_info},',
290
- f'File={t_node} not found in directory={t_path}')
312
+ f'File={t_node} not found in directory={t_path}',
313
+ error_code=EDA_DEPS_FILE_NOT_FOUND)
291
314
  elif not self.rel_deps_file:
292
315
  # target, but there's no DEPS file
293
316
  self.error(f'Trying to resolve target={t_full}:',
294
317
  f'called from {caller_info},',
295
- f'but {t_path} has no DEPS markup file (DEPS.yml)')
318
+ f'but {t_path} has no DEPS markup file (DEPS.yml)',
319
+ error_code=EDA_DEPS_FILE_NOT_FOUND)
296
320
  else:
297
321
  self.error(f'Trying to resolve target={t_full}:',
298
322
  f'called from {caller_info},',
299
- f'Target not found in deps_file={self.rel_deps_file}')
323
+ f'Target not found in deps_file={self.rel_deps_file}',
324
+ error_code=EDA_DEPS_TARGET_NOT_FOUND)
300
325
  else:
301
326
  debug(f'Found {target_node=} in deps_file={self.rel_deps_file}')
302
327
  found_target = True
@@ -324,3 +349,31 @@ class DepsFile:
324
349
  entry.update(entry_sanitized)
325
350
 
326
351
  return entry
352
+
353
+ def get_all_targets(self) -> list:
354
+ '''Returns list of all targets in this obj, skipping keys like DEFAULTS and METADATA'''
355
+ return deps_data_get_all_targets(self.data)
356
+
357
+ def get_sanitized_data(self) -> dict:
358
+ '''Returns a sanitized dict of self.data, resolves DEFAULTS, METADATA, and implicit
359
+
360
+ space-separated-str, lists for 'deps' in a target entry
361
+ '''
362
+ new_data = {}
363
+ targets = self.get_all_targets()
364
+ for target in targets:
365
+ new_data[target] = self.get_entry(target)
366
+ return new_data
367
+
368
+ def write_sanitized_markup(self, filepath: str) -> None:
369
+ '''Writes the sanitized dict of self.data as JSON/YAML (filepath extension)'''
370
+ markup_writer(data=self.get_sanitized_data(), filepath=filepath)
371
+
372
+ def str_sanitized_markup(self, as_yaml: bool = False) -> str:
373
+ '''Returns str sanitized YAML or JSON str of self.data'''
374
+ return markup_dumper(self.get_sanitized_data(), as_yaml=as_yaml)
375
+
376
+
377
+ def print_sanitized_markup(self, as_yaml: bool = False) -> None:
378
+ '''Prints sanitized JSON str of self.data to STDOUT'''
379
+ print(self.str_sanitized_markup(as_yaml=as_yaml))
@@ -16,9 +16,12 @@ import argparse
16
16
  import shlex
17
17
  import importlib.util
18
18
 
19
+ from pathlib import Path
20
+
19
21
  import opencos
20
22
  from opencos import util, eda_config, eda_base
21
23
  from opencos.eda_base import Tool, which_tool
24
+ from opencos.utils import vsim_helper
22
25
 
23
26
  # Configure util:
24
27
  util.progname = "EDA"
@@ -151,6 +154,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
151
154
  If so, updates config['auto_tools_order'][tool]['exe']
152
155
  '''
153
156
 
157
+
154
158
  tool = eda_config.update_config_auto_tool_order_for_tool(
155
159
  tool=tool, config=config
156
160
  )
@@ -192,16 +196,23 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
192
196
  has_all_exe = True
193
197
  has_all_in_exe_path = True
194
198
  for exe in exe_list:
195
- assert exe != '', f'{tool=} {value=} value missing "exe" {exe=}'
199
+ assert exe != '', f'{name=} {value=} value missing "exe" {exe=}'
196
200
  p = shutil.which(exe)
197
201
  if not p:
198
202
  has_all_exe = False
199
203
  util.debug(f"... No, missing exe {exe}")
200
204
  for req in value.get('requires_in_exe_path', []):
201
- if p and req and str(req).lower() not in str(p).lower():
205
+ if p and req and str(Path(req)) not in str(Path(p)):
202
206
  has_all_in_exe_path = False
203
207
  util.debug(f"... No, missing path requirement {req}")
204
208
 
209
+ has_vsim_helper = True
210
+ if value.get('requires_vsim_helper', False):
211
+ # This tool name must be in opencos.utils.vsim_helper.TOOL_IS[name].
212
+ # Special case for vsim being used by a lot of tools.
213
+ vsim_helper.init() # only runs checks once internally
214
+ has_vsim_helper = vsim_helper.TOOL_IS.get(name, False)
215
+
205
216
  if has_all_exe:
206
217
  requires_cmd_list = value.get('requires_cmd', [])
207
218
  for cmd in requires_cmd_list:
@@ -219,7 +230,8 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
219
230
  util.debug(f"... No, exception {e} running {cmd_list}")
220
231
 
221
232
 
222
- if all([has_all_py, has_all_env, has_all_exe, has_all_in_exe_path]):
233
+ if all([has_all_py, has_all_env, has_all_exe, has_all_in_exe_path,
234
+ has_vsim_helper]):
223
235
  exe = exe_list[0]
224
236
  p = shutil.which(exe)
225
237
  config['auto_tools_found'][name] = exe # populate key-value pairs w/ first exe in list
@@ -28,6 +28,7 @@ from opencos.util import Colors
28
28
  from opencos.utils.str_helpers import sprint_time, strip_outer_quotes, string_or_space, \
29
29
  indent_wrap_long_text
30
30
  from opencos.utils.subprocess_helpers import subprocess_run_background
31
+ from opencos.utils import status_constants
31
32
 
32
33
  from opencos.deps.deps_file import DepsFile, deps_data_get_all_targets
33
34
  from opencos.deps.deps_processor import DepsProcessor
@@ -257,10 +258,15 @@ class Command:
257
258
  if self.args['work-dir']:
258
259
  if not self.errors_log_f:
259
260
  try:
261
+ fullpath = os.path.join(self.args['work-dir'], 'eda.errors.log')
260
262
  self.errors_log_f = open( # pylint: disable=consider-using-with
261
- os.path.join(self.args['work-dir'], 'eda.errors.log'), 'w',
262
- encoding='utf-8'
263
+ fullpath, 'w', encoding='utf-8'
263
264
  )
265
+ util.artifacts.add(
266
+ name=fullpath,
267
+ typ='text', description='EDA reported errors'
268
+ )
269
+
264
270
  except FileNotFoundError:
265
271
  pass
266
272
  if self.errors_log_f:
@@ -269,7 +275,7 @@ class Command:
269
275
  file=self.errors_log_f
270
276
  )
271
277
 
272
- self.status = util.error(*args, **kwargs)
278
+ self.status = util.error(*args, **kwargs) # error_code passed and returned via kwargs
273
279
 
274
280
  def status_any_error(self, report=True) -> bool:
275
281
  '''Used by derived classes process_tokens() to know an error was reached
@@ -374,8 +380,16 @@ class Command:
374
380
  if self.args['keep']:
375
381
  open(keep_file, 'w', encoding='utf-8').close() # pylint: disable=consider-using-with
376
382
  util.debug(f'create_work_dir: created {keep_file=}')
383
+
384
+ # Set the util.artifacts path with our work-dir:
385
+ util.artifacts.set_artifacts_json_dir(self.args['work-dir'])
386
+
377
387
  return self.args['work-dir']
378
388
 
389
+ def artifacts_add(self, name: str, typ: str, description: str) -> None:
390
+ '''Adds a file to util.artifacts, derived classes may override'''
391
+ util.artifacts.add(name=name, typ=typ, description=description)
392
+
379
393
 
380
394
  def exec(self, work_dir: str, command_list: list, background: bool = False,
381
395
  stop_on_error: bool = True, quiet: bool = False, tee_fpath: str = '',
@@ -403,10 +417,16 @@ class Command:
403
417
  shell=shell
404
418
  )
405
419
 
406
- if return_code:
407
- self.status += return_code
420
+ if return_code > 0:
421
+ if return_code == 1:
422
+ self.status = status_constants.EDA_EXEC_NONZERO_RETURN_CODE1
423
+ if return_code == 255:
424
+ self.status = status_constants.EDA_EXEC_NONZERO_RETURN_CODE255
425
+ else:
426
+ self.status = status_constants.EDA_EXEC_NONZERO_RETURN_CODE2
408
427
  if stop_on_error:
409
- self.error(f"exec: returned with error (return code: {return_code})")
428
+ self.error(f"exec: returned with error (return code: {return_code})",
429
+ error_code=self.status)
410
430
  else:
411
431
  util.debug(f"exec: returned with error (return code: {return_code})")
412
432
  else:
@@ -424,7 +444,8 @@ class Command:
424
444
  # Do some minimal type handling, preserving the type(self.args[key])
425
445
  if key not in self.args:
426
446
  self.error(f'set_arg, {key=} not in self.args {value=}',
427
- f'({self.command_name=}, {self.__class__.__name__=})')
447
+ f'({self.command_name=}, {self.__class__.__name__=})',
448
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
428
449
 
429
450
  cur_value = self.args[key]
430
451
 
@@ -555,7 +576,8 @@ class Command:
555
576
  parsed, unparsed = parser.parse_known_args(tokens + [''])
556
577
  unparsed = list(filter(None, unparsed))
557
578
  except argparse.ArgumentError:
558
- self.error(f'problem {self.command_name=} attempting to parse_known_args for {tokens=}')
579
+ self.error(f'problem {self.command_name=} attempting to parse_known_args for {tokens=}',
580
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
559
581
 
560
582
  parsed_as_dict = vars(parsed)
561
583
 
@@ -617,7 +639,8 @@ class Command:
617
639
  _, unparsed = self.run_argparser_on_list(tokens)
618
640
  if process_all and len(unparsed) > 0:
619
641
  self.error(f"Didn't understand argument: '{unparsed=}' in",
620
- f" {self.command_name=} context, {pwd=}")
642
+ f" {self.command_name=} context, {pwd=}",
643
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
621
644
 
622
645
  return unparsed
623
646
 
@@ -637,7 +660,8 @@ class Command:
637
660
 
638
661
  if not ret and error_if_no_command:
639
662
  self.error(f"Looking for a valid eda {self.command_name} <command>",
640
- f"but didn't find one in {tokens=}")
663
+ f"but didn't find one in {tokens=}",
664
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
641
665
  return ret
642
666
 
643
667
 
@@ -679,7 +703,8 @@ class Command:
679
703
  process_tokens(..) is the starting entrypoint from eda.py'''
680
704
  self.write_eda_config_and_args()
681
705
  self.error(f"No tool bound to command '{self.command_name}', you",
682
- " probably need to setup tool, or use '--tool <name>'")
706
+ " probably need to setup tool, or use '--tool <name>'",
707
+ error_code=status_constants.EDA_CONFIG_ERROR)
683
708
 
684
709
  def command_safe_set_tool_defines(self) -> None:
685
710
  '''Safe wrapper for calling self.set_tool_defines() in case a Tool parent class is
@@ -993,7 +1018,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
993
1018
  self.defines[k] = v
994
1019
  util.debug(f"Defined {k}={v}")
995
1020
  return None
996
- self.error(f"Didn't understand +define+: '{plusarg}'")
1021
+ self.error(f"Didn't understand +define+: '{plusarg}'",
1022
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
997
1023
  return None
998
1024
 
999
1025
  if plusarg.startswith('+incdir+'):
@@ -1005,7 +1031,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1005
1031
  self.incdirs.append(os.path.abspath(incdir))
1006
1032
  util.debug(f"Added include dir '{os.path.abspath(incdir)}'")
1007
1033
  return None
1008
- self.error(f"Didn't understand +incdir+: '{plusarg}'")
1034
+ self.error(f"Didn't understand +incdir+: '{plusarg}'",
1035
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1009
1036
  return None
1010
1037
 
1011
1038
  # remaining plusargs as stored in self.args['unprocessed-plusargs'] (list)
@@ -1019,7 +1046,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1019
1046
  # derived classes have the option to handle it
1020
1047
  return plusarg
1021
1048
 
1022
- self.error(f"Didn't understand +plusarg: '{plusarg}'")
1049
+ self.error(f"Didn't understand +plusarg: '{plusarg}'",
1050
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1023
1051
  return None
1024
1052
 
1025
1053
 
@@ -1211,7 +1239,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1211
1239
  found_target = True
1212
1240
  break # move on to the next target
1213
1241
  if not found_target: # if STILL not found_this_target...
1214
- self.error(f"Unable to resolve {target=}")
1242
+ self.error(f"Unable to resolve {target=}",
1243
+ error_code=status_constants.EDA_DEPS_TARGET_NOT_FOUND)
1215
1244
 
1216
1245
  # if we've found any target since being called, it means we found the one we were called for
1217
1246
  return found_target
@@ -1341,7 +1370,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1341
1370
  if len(unparsed) == 0:
1342
1371
  # If unparsed is still empty, then error.
1343
1372
  self.error(f"For command '{self.command_name}' no files or targets were",
1344
- f"presented at the command line: {orig_tokens}")
1373
+ f"presented at the command line: {orig_tokens}",
1374
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1345
1375
 
1346
1376
  # by this point hopefully this is a target ... is it a simple filename?
1347
1377
  remove_list = []
@@ -1395,7 +1425,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1395
1425
 
1396
1426
  # we were unable to figure out what this command line token is for...
1397
1427
  if process_all and len(unparsed) > 0:
1398
- self.error(f"Didn't understand command remaining tokens {unparsed=} in CommandDesign")
1428
+ self.error(f"Didn't understand command remaining tokens {unparsed=} in CommandDesign",
1429
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1399
1430
 
1400
1431
  # handle a missing self.args['top'] with last filepath or last target:
1401
1432
  if not self.args.get('top', ''):
@@ -1428,7 +1459,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1428
1459
 
1429
1460
  if self.error_on_missing_top and not self.args.get('top', ''):
1430
1461
  self.error("Did not get a --top or DEPS top, required to run command",
1431
- f"'{self.command_name}' for tool={self.args.get('tool', None)}")
1462
+ f"'{self.command_name}' for tool={self.args.get('tool', None)}",
1463
+ error_code=status_constants.EDA_COMMAND_MISSING_TOP)
1432
1464
 
1433
1465
  return unparsed
1434
1466
 
@@ -1930,7 +1962,8 @@ class CommandParallel(Command):
1930
1962
  bad_remaining_args = [x for x in single_cmd_unparsed if x.startswith('-')]
1931
1963
  if bad_remaining_args:
1932
1964
  self.error(f'for {self.command_name} {command=} the following args are unknown',
1933
- f'{bad_remaining_args}')
1965
+ f'{bad_remaining_args}',
1966
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1934
1967
 
1935
1968
  # Remove unparsed args starting with '+', since those are commonly sent downstream to
1936
1969
  # single job (example, CommandSim plusargs).
@@ -1972,7 +2005,8 @@ class CommandParallel(Command):
1972
2005
 
1973
2006
  key = get_job_arg(job_dict, arg_name='job-name')
1974
2007
  if not key:
1975
- self.error(f'{job_dict=} needs to have a --job-name= arg attached')
2008
+ self.error(f'{job_dict=} needs to have a --job-name= arg attached',
2009
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1976
2010
  if key not in job_names_count_dict:
1977
2011
  job_names_count_dict[key] = 1
1978
2012
  else:
@@ -46,6 +46,7 @@ class Defaults:
46
46
  supported_config_auto_tools_order_keys = set([
47
47
  'exe', 'handlers',
48
48
  'requires_env', 'requires_py', 'requires_cmd', 'requires_in_exe_path',
49
+ 'requires_vsim_helper',
49
50
  'disable-tools-multi',
50
51
  ])
51
52
  supported_config_tool_keys = set([
@@ -308,6 +309,11 @@ def write_eda_config_and_args(
308
309
  # Use deep copy b/c otherwise these are references to opencos.eda.
309
310
  data[x] = copy.deepcopy(getattr(command_obj_ref, x, ''))
310
311
 
312
+ # copy util.args
313
+ data['util'] = {
314
+ 'args': util.args
315
+ }
316
+
311
317
  # fix some burried class references in command_obj_ref.config,
312
318
  # otherwise we won't be able to safe load this yaml, so cast as str repr.
313
319
  for k, v in getattr(command_obj_ref, 'config', {}).items():