opencos-eda 0.3.5__tar.gz → 0.3.6__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 (102) hide show
  1. {opencos_eda-0.3.5/opencos_eda.egg-info → opencos_eda-0.3.6}/PKG-INFO +4 -3
  2. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/sim.py +16 -6
  3. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/deps/deps_processor.py +13 -2
  4. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda.py +6 -5
  5. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_base.py +77 -29
  6. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_config.py +2 -1
  7. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_config_defaults.yml +5 -1
  8. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/export_helper.py +89 -31
  9. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/files.py +3 -1
  10. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/helpers.py +23 -17
  11. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/cocotb.py +94 -21
  12. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/verilator.py +5 -2
  13. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/util.py +73 -50
  14. {opencos_eda-0.3.5 → opencos_eda-0.3.6/opencos_eda.egg-info}/PKG-INFO +4 -3
  15. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos_eda.egg-info/requires.txt +3 -2
  16. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/pyproject.toml +5 -3
  17. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/LICENSE +0 -0
  18. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/LICENSE.spdx +0 -0
  19. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/README.md +0 -0
  20. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/__init__.py +0 -0
  21. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/_version.py +0 -0
  22. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/_waves_pkg.sv +0 -0
  23. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/__init__.py +0 -0
  24. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/build.py +0 -0
  25. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/deps_help.py +0 -0
  26. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/elab.py +0 -0
  27. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/export.py +0 -0
  28. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/flist.py +0 -0
  29. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/lec.py +0 -0
  30. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/lint.py +0 -0
  31. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/multi.py +0 -0
  32. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/open.py +0 -0
  33. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/proj.py +0 -0
  34. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/shell.py +0 -0
  35. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/sweep.py +0 -0
  36. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/synth.py +0 -0
  37. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/targets.py +0 -0
  38. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/upload.py +0 -0
  39. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/commands/waves.py +0 -0
  40. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/deps/__init__.py +0 -0
  41. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/deps/defaults.py +0 -0
  42. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/deps/deps_commands.py +0 -0
  43. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/deps/deps_file.py +0 -0
  44. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/deps_schema.py +0 -0
  45. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_config_max_verilator_waivers.yml +0 -0
  46. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_config_reduced.yml +0 -0
  47. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_deps_bash_completion.bash +0 -0
  48. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_deps_sanitize.py +0 -0
  49. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_extract_targets.py +0 -0
  50. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/eda_tool_helper.py +0 -0
  51. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/export_json_convert.py +0 -0
  52. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/hw/__init__.py +0 -0
  53. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/hw/oc_cli.py +0 -0
  54. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/hw/pcie.py +0 -0
  55. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/names.py +0 -0
  56. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/peakrdl_cleanup.py +0 -0
  57. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/seed.py +0 -0
  58. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/__init__.py +0 -0
  59. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/custom_config.yml +0 -0
  60. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/deps_files/command_order/DEPS.yml +0 -0
  61. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/deps_files/error_msgs/DEPS.yml +0 -0
  62. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -0
  63. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  64. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -0
  65. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -0
  66. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -0
  67. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_build.py +0 -0
  68. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_deps_helpers.py +0 -0
  69. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_deps_schema.py +0 -0
  70. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_eda.py +0 -0
  71. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_eda_elab.py +0 -0
  72. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_eda_synth.py +0 -0
  73. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_oc_cli.py +0 -0
  74. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tests/test_tools.py +0 -0
  75. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/__init__.py +0 -0
  76. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/invio.py +0 -0
  77. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/invio_helpers.py +0 -0
  78. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/invio_yosys.py +0 -0
  79. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/iverilog.py +0 -0
  80. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/modelsim_ase.py +0 -0
  81. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/quartus.py +0 -0
  82. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/questa.py +0 -0
  83. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/questa_fse.py +0 -0
  84. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/riviera.py +0 -0
  85. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/slang.py +0 -0
  86. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/slang_yosys.py +0 -0
  87. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/surelog.py +0 -0
  88. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/tabbycad_yosys.py +0 -0
  89. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/vivado.py +0 -0
  90. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/tools/yosys.py +0 -0
  91. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/utils/__init__.py +0 -0
  92. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/utils/markup_helpers.py +0 -0
  93. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/utils/status_constants.py +0 -0
  94. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/utils/str_helpers.py +0 -0
  95. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/utils/subprocess_helpers.py +0 -0
  96. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/utils/vscode_helper.py +0 -0
  97. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos/utils/vsim_helper.py +0 -0
  98. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos_eda.egg-info/SOURCES.txt +0 -0
  99. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos_eda.egg-info/dependency_links.txt +0 -0
  100. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos_eda.egg-info/entry_points.txt +0 -0
  101. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/opencos_eda.egg-info/top_level.txt +0 -0
  102. {opencos_eda-0.3.5 → opencos_eda-0.3.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.3.5
3
+ Version: 0.3.6
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
@@ -16,11 +16,12 @@ Requires-Dist: schema>=0.7.7
16
16
  Requires-Dist: toml>=0.10.2
17
17
  Requires-Dist: yamllint>=1.35.1
18
18
  Requires-Dist: PySerial>=3.5
19
- Requires-Dist: cocotb>=2.0
20
19
  Requires-Dist: supports_color>=0.2.0
20
+ Requires-Dist: cocotb>=2.0
21
+ Requires-Dist: pytest>=8.3.5
22
+ Requires-Dist: coverage>=7.6.1
21
23
  Provides-Extra: dev
22
24
  Requires-Dist: pylint>=3.0.0; extra == "dev"
23
- Requires-Dist: pytest>=8.3.5; extra == "dev"
24
25
  Provides-Extra: docs
25
26
  Requires-Dist: mkdocs; extra == "docs"
26
27
  Requires-Dist: mkdocs-material; extra == "docs"
@@ -145,6 +145,7 @@ class CommandSim(CommandDesign):
145
145
  self.run_dep_commands()
146
146
  self.do_it()
147
147
  self.run_post_tool_dep_commands()
148
+ self.report_pass_fail()
148
149
  return unparsed
149
150
 
150
151
 
@@ -198,6 +199,9 @@ class CommandSim(CommandDesign):
198
199
 
199
200
  clist = list(obj).copy()
200
201
  tee_fpath = getattr(obj, 'tee_fpath', None)
202
+ work_dir = getattr(obj, 'work_dir', None)
203
+ if not work_dir:
204
+ work_dir = self.args['work-dir']
201
205
 
202
206
  util.debug(f'run_commands_check_logs: {clist=}, {tee_fpath=}')
203
207
 
@@ -207,24 +211,28 @@ class CommandSim(CommandDesign):
207
211
  if log_filename:
208
212
  log_fname = log_filename
209
213
 
214
+
210
215
  _, stdout, _ = self.exec(
211
- work_dir=self.args['work-dir'], command_list=clist, tee_fpath=tee_fpath
216
+ work_dir=work_dir, command_list=clist, tee_fpath=tee_fpath
212
217
  )
213
218
 
214
219
  if check_logs and log_fname:
215
220
  # Note this call will check on stdout if not GUI, not opening the log_fname,
216
221
  # but if this is GUI we normally lose stdout and have to open the log.
217
- gui_mode = self.args.get('gui', False)
218
- file_contents_str = '' if gui_mode else stdout
222
+ if self.args.get('gui', False):
223
+ file_contents_str = ''
224
+ else:
225
+ file_contents_str = stdout
226
+
219
227
  self.check_logs_for_errors(
220
- filename=os.path.join(self.args['work-dir'], log_fname),
228
+ filename=os.path.join(work_dir, log_fname),
221
229
  file_contents_str=file_contents_str,
222
230
  bad_strings=bad_strings, must_strings=must_strings,
223
231
  use_bad_strings=use_bad_strings, use_must_strings=use_must_strings
224
232
  )
225
233
  if log_fname:
226
234
  self.artifacts_add(
227
- name=os.path.join(self.args['work-dir'], log_fname),
235
+ name=os.path.join(work_dir, log_fname),
228
236
  typ='text', description='Simulator stdout/stderr log file'
229
237
  )
230
238
 
@@ -253,6 +261,7 @@ class CommandSim(CommandDesign):
253
261
  tool = self.args.get('tool', None)
254
262
  # Certain args are allow-listed here
255
263
  deps_file_args = []
264
+ print(f'SUPER DREW DEBUG: {self.get_command_line_args()=}')
256
265
  for a in self.get_command_line_args():
257
266
  if any(a.startswith(x) for x in [
258
267
  '--compile-args',
@@ -265,7 +274,8 @@ class CommandSim(CommandDesign):
265
274
  '--stop-',
266
275
  '--lint-',
267
276
  '--verilate',
268
- '--verilator']):
277
+ '--verilator',
278
+ '--cocotb-test-']):
269
279
  deps_file_args.append(a)
270
280
 
271
281
  export_obj.run(
@@ -10,7 +10,7 @@ import os
10
10
  from opencos import files
11
11
  from opencos import eda_config
12
12
  from opencos.util import debug, info, warning, error, read_tokens_from_dot_f, \
13
- patch_args_for_dir
13
+ patch_args_for_dir, load_env_file
14
14
  from opencos.utils.str_helpers import dep_str2list
15
15
  from opencos.deps.deps_file import deps_target_get_deps_list
16
16
  from opencos.deps.deps_commands import deps_commands_handler
@@ -192,9 +192,15 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
192
192
  # Since some args (util.py, eda_config.py, eda.py) can only be handled from command
193
193
  # line, it would be nice if -f or --input-file is handled from DEPS, so we'll special
194
194
  # case that now. Recursively resolve -f / --input-file.
195
+ # Do similary for --env-file (also only supported in util.py)
195
196
  parser = argparse.ArgumentParser(
196
197
  prog='deps_processor -f/--input-file', add_help=False, allow_abbrev=False
197
198
  )
199
+ parser.add_argument('--env-file', default=[], action='append',
200
+ help=(
201
+ "dotenv file(s) to pass ENV vars, (default: .env loaded first,"
202
+ " subsequent files' vars override .env"
203
+ ))
198
204
  parser.add_argument('-f', '--input-file', default=[], action='append',
199
205
  help=(
200
206
  'Input .f file to be expanded as eda args, defines, incdirs,'
@@ -225,7 +231,12 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
225
231
  # recurse until we've resolved nested .f files.
226
232
  return self.apply_args(args_list=tokens2)
227
233
 
228
- tokens = tokens2 # if no --input-file values, keep parsing the remaining tokens2
234
+ if parsed.env_file:
235
+ for env_file in parsed.env_file:
236
+ load_env_file(env_file)
237
+
238
+ # if no --input-file/--env-file values, keep parsing the remaining tokens2:
239
+ tokens = tokens2
229
240
 
230
241
  # We have to special-case anything with --tool[=value] in tokens, otherwise
231
242
  # the user may think they were allowed to set --tool, but in our flow the Command handler
@@ -20,6 +20,7 @@ from pathlib import Path
20
20
 
21
21
  import opencos
22
22
  from opencos import util, eda_config, eda_base
23
+ from opencos.util import safe_emoji
23
24
  from opencos.eda_base import Tool, which_tool, get_eda_exec
24
25
  from opencos.utils import vsim_helper, vscode_helper
25
26
  from opencos.utils.subprocess_helpers import subprocess_run_background
@@ -95,15 +96,15 @@ def usage(tokens: list, config: dict, command: str = "", tool: str = "") -> int:
95
96
 
96
97
  if command == "":
97
98
  print(
98
- """
99
- Usage:
99
+ f"""
100
+ {safe_emoji("🔦 ")}Usage:
100
101
  eda [<options>] <command> [options] <files|targets, ...>
101
102
  """
102
103
  )
103
104
  print(get_all_commands_help_str(config=config))
104
105
  print(
105
- """
106
- And <files|targets, ...> is one or more source file or DEPS markup file target,
106
+ f"""
107
+ {safe_emoji("❕ ")}where <files|targets, ...> is one or more source file or DEPS markup file target,
107
108
  such as .v, .sv, .vhd[l], .cpp files, or a target key in a DEPS.[yml|yaml|toml|json].
108
109
  Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` to
109
110
  force use that file as systemverilog, verilog, vhdl, or C++, respectively.
@@ -495,7 +496,7 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
495
496
  def signal_handler(sig, frame) -> None: # pylint: disable=unused-argument
496
497
  '''Handles Ctrl-C, called by main_cli() if running from command line'''
497
498
  util.fancy_stop()
498
- util.info('Received Ctrl+C...', start='\nINFO: [EDA] ')
499
+ util.error(f'{safe_emoji("❌ ")}Received Ctrl+C...', start='\nINFO: [EDA] ')
499
500
  subprocess_helpers.cleanup_all()
500
501
  util.exit(-1)
501
502
 
@@ -24,7 +24,7 @@ from pathlib import Path
24
24
  from opencos import seed, util, files
25
25
  from opencos import eda_config
26
26
 
27
- from opencos.util import Colors
27
+ from opencos.util import Colors, safe_emoji
28
28
  from opencos.utils.str_helpers import sprint_time, strip_outer_quotes, string_or_space, \
29
29
  indent_wrap_long_text, pretty_list_columns_manual
30
30
  from opencos.utils.subprocess_helpers import subprocess_run_background
@@ -44,7 +44,9 @@ def print_base_help() -> None:
44
44
 
45
45
  def get_argparser() -> argparse.ArgumentParser:
46
46
  '''Returns the ArgumentParser for general eda CLI'''
47
- parser = argparse.ArgumentParser(prog='eda options', add_help=False, allow_abbrev=False)
47
+ parser = argparse.ArgumentParser(
48
+ prog=f'{safe_emoji("🔎 ")}eda options', add_help=False, allow_abbrev=False
49
+ )
48
50
  parser.add_argument('-q', '--quit', action='store_true',
49
51
  help=(
50
52
  'For interactive mode (eda called with no options, command, or'
@@ -313,6 +315,18 @@ class Command: # pylint: disable=too-many-public-methods
313
315
  util.error(f"command '{self.command_name}' has previous errors")
314
316
  return self.status > 0
315
317
 
318
+ def report_pass_fail(self) -> None:
319
+ '''Reports an INFO line with pass/fail information'''
320
+ job_name = ' - '.join(
321
+ x for x in (self.command_name, self.args.get('tool', ''),
322
+ self.args.get('top', '')) if x
323
+ )
324
+ if self.status_any_error():
325
+ util.info(f'{safe_emoji("❌ ")}{job_name}: Errors observed.', color=Colors.red)
326
+ else:
327
+ util.info(f'{safe_emoji("✅ ")}{job_name}: No errors observed.')
328
+
329
+
316
330
  def which_tool(self, command:str) -> str:
317
331
  '''Returns a str for the tool name used for the requested command'''
318
332
  return which_tool(command, config=self.config)
@@ -462,7 +476,8 @@ class Command: # pylint: disable=too-many-public-methods
462
476
  if not tee_fpath and getattr(command_list, 'tee_fpath', None):
463
477
  tee_fpath = getattr(command_list, 'tee_fpath', '')
464
478
  if not quiet:
465
- util.info(f"exec: {' '.join(command_list)} (in {work_dir}, {tee_fpath=})")
479
+ util.info(f"{safe_emoji('⏩ ')}exec: {' '.join(command_list)}",
480
+ f"(in {work_dir}, {tee_fpath=})")
466
481
 
467
482
  stdout, stderr, return_code = subprocess_run_background(
468
483
  work_dir=work_dir,
@@ -484,7 +499,8 @@ class Command: # pylint: disable=too-many-public-methods
484
499
  self.error(f"exec: returned with error (return code: {return_code})",
485
500
  error_code=self.status)
486
501
  else:
487
- util.debug(f"exec: returned with error (return code: {return_code})")
502
+ util.debug(f"{safe_emoji('❌ ')}exec: returned with error (return code:",
503
+ f"{return_code})")
488
504
  else:
489
505
  util.debug(f"exec: returned without error (return code: {return_code})")
490
506
  return stderr, stdout, return_code
@@ -564,7 +580,9 @@ class Command: # pylint: disable=too-many-public-methods
564
580
  # parsed.args-with-dashes is not legal python. Some of self.args.keys() still have - or _,
565
581
  # so this will handle both.
566
582
  # Also, preference is for self.args.keys(), to be str with - dashes
567
- parser = argparse.ArgumentParser(prog='eda', add_help=False, allow_abbrev=False)
583
+ parser = argparse.ArgumentParser(
584
+ prog=f'{safe_emoji("🔎 ")}eda', add_help=False, allow_abbrev=False
585
+ )
568
586
  bool_action_kwargs = util.get_argparse_bool_action_kwargs()
569
587
 
570
588
  if not parser_arg_list:
@@ -797,7 +815,7 @@ class Command: # pylint: disable=too-many-public-methods
797
815
  util.info('Help:')
798
816
  # using bare 'print' here, since help was requested, avoids --color and --quiet
799
817
  print()
800
- print('Usage:')
818
+ print(f'{safe_emoji("🔦 ")}Usage:')
801
819
  if no_targets:
802
820
  print(f' eda [options] {self.command_name} [options]')
803
821
  else:
@@ -811,10 +829,10 @@ class Command: # pylint: disable=too-many-public-methods
811
829
  return
812
830
 
813
831
  if self.command_name:
814
- lines.append(f"Generic help for command='{self.command_name}'"
832
+ lines.append(f"{safe_emoji('🔧 ')}Generic help for command='{self.command_name}'"
815
833
  f" (using '{self.__class__.__name__}')")
816
834
  else:
817
- lines.append("Generic help (from class Command):")
835
+ lines.append("{safe_emoji('🔧 ')}Generic help (from class Command):")
818
836
 
819
837
  # Attempt to run argparser on args, but don't error if it fails.
820
838
  unparsed = []
@@ -980,6 +998,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
980
998
  self.files_vhd = []
981
999
  self.files_cpp = []
982
1000
  self.files_sdc = []
1001
+ self.files_py = []
1002
+ self.files_makefile = []
983
1003
  self.files_non_source = []
984
1004
  self.files_caller_info = {}
985
1005
  self.dep_shell_commands = [] # each list entry is a {}
@@ -1139,7 +1159,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1139
1159
  self.files[new_key] = True
1140
1160
 
1141
1161
  my_file_lists_list = [self.files_v, self.files_sv, self.files_vhd, self.files_cpp,
1142
- self.files_sdc]
1162
+ self.files_sdc, self.files_py, self.files_makefile]
1143
1163
  for my_file_list in my_file_lists_list:
1144
1164
  for i,value in enumerate(my_file_list):
1145
1165
  if value and isinstance(value, str) and \
@@ -1157,9 +1177,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1157
1177
  need to be copied or linked to the work-dir. For example, if some SV assumes it
1158
1178
  can $readmemh('file_that_is_here.txt') but we're running out of work-dir. Linking
1159
1179
  is the easy work-around vs trying to run-in-place of all SV files.
1180
+
1181
+ Note that we also include .py and Makefile(s) in this.
1160
1182
  '''
1161
1183
 
1162
- for fname in self.files_non_source:
1184
+ for fname in self.files_non_source + self.files_py + self.files_makefile:
1163
1185
  _, leaf_fname = os.path.split(fname)
1164
1186
  destfile = os.path.join(self.args['work-dir'], leaf_fname)
1165
1187
  relfname = os.path.relpath(fname)
@@ -1518,7 +1540,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1518
1540
  # if we've found any target since being called, it means we found the one we were called for
1519
1541
  return found_target
1520
1542
 
1521
- def add_file( # pylint: disable=too-many-locals,too-many-branches
1543
+ def add_file( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
1522
1544
  self, filename: str, use_abspath: bool = True, add_to_non_sources: bool = False,
1523
1545
  caller_info: str = '', forced_extension: str = ''
1524
1546
  ) -> str:
@@ -1543,6 +1565,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1543
1565
  cpp_file_ext_list = known_file_ext_dict.get('cpp', [])
1544
1566
  sdc_file_ext_list = known_file_ext_dict.get('synth_constraints', [])
1545
1567
  dotf_file_ext_list = known_file_ext_dict.get('dotf', [])
1568
+ py_file_ext_list = known_file_ext_dict.get('python', [])
1569
+ makefile_ext_list = known_file_ext_dict.get('makefile', [])
1546
1570
 
1547
1571
  if forced_extension:
1548
1572
  # If forced_extension='systemverilog', then use the first known extension for
@@ -1582,6 +1606,12 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1582
1606
  caller_info=caller_info)
1583
1607
  dp.apply_args(args_list=[f'-f={file_abspath}'])
1584
1608
  del dp
1609
+ elif file_ext in py_file_ext_list:
1610
+ self.files_py.append(file_abspath)
1611
+ util.debug(f"Added Python file {filename} as {file_abspath}")
1612
+ elif file_ext in makefile_ext_list or os.path.split(filename)[1] == 'Makefile':
1613
+ self.files_makefile.append(file_abspath)
1614
+ util.debug(f"Added Makefile {filename} as {file_abspath}")
1585
1615
  else:
1586
1616
  # unknown file extension. In these cases we link the file to the working directory
1587
1617
  # so it is available (for example, a .mem file that is expected to exist with relative
@@ -2114,21 +2144,22 @@ class CommandParallel(Command):
2114
2144
  work_queue.put((jobs_launched, command_list, job['name'], cwd))
2115
2145
  suffix = "<START>"
2116
2146
  if fancy_mode:
2117
- util.fancy_print(job_text+suffix, worker)
2147
+ util.fancy_print(job_text + suffix, worker)
2118
2148
  elif failed_jobs:
2119
2149
  # if we aren't in fancy mode, we will print a START line, periodic RUNNING
2120
2150
  # lines, and PASS/FAIL line per-job
2121
- util.print_orange(job_text + Colors.yellow + suffix)
2151
+ util.print_yellow(job_text, end='')
2152
+ util.print_foreground_color(suffix)
2122
2153
  else:
2123
- util.print_yellow(job_text + Colors.yellow + suffix)
2154
+ util.print_foreground_color(job_text + suffix)
2124
2155
  else:
2125
2156
  # single-threaded job launch, we are going to print out job info as we start
2126
2157
  # each job... no newline. since non-verbose silences the job and prints only
2127
2158
  # <PASS>/<FAIL> after the trailing "..." we leave here
2128
2159
  if failed_jobs:
2129
- util.print_orange(job_text, end="")
2130
- else:
2131
2160
  util.print_yellow(job_text, end="")
2161
+ else:
2162
+ util.print_foreground_color(job_text, end="")
2132
2163
  job_done_number = jobs_launched
2133
2164
  job_done_name = job['name']
2134
2165
  job_start_time = time.time()
@@ -2182,9 +2213,10 @@ class CommandParallel(Command):
2182
2213
  if fancy_mode:
2183
2214
  util.fancy_print(f"{job_text}{suffix}", t['worker'])
2184
2215
  elif failed_jobs:
2185
- util.print_orange(job_text + Colors.yellow + suffix)
2216
+ util.print_yellow(job_text, end='')
2217
+ util.print_foreground_color(suffix)
2186
2218
  else:
2187
- util.print_yellow(job_text + Colors.yellow + suffix)
2219
+ util.print_foreground_color(job_text + suffix)
2188
2220
 
2189
2221
  # shared job completion code
2190
2222
  # single or multi-threaded, we can arrive here to harvest <= 1 jobs, and need
@@ -2192,36 +2224,52 @@ class CommandParallel(Command):
2192
2224
  # printed, ready for pass/fail
2193
2225
  if job_done:
2194
2226
  jobs_complete += 1
2227
+ this_job_failed = False
2195
2228
  if job_done_return_code is None or job_done_return_code:
2196
- # embed the color code, to change color of pass/fail during the
2197
- # util.print_orange/yellow below
2198
2229
  if job_done_return_code == 124:
2199
2230
  # bash uses 124 for bash timeout errors, if that was preprended to the
2200
2231
  # command list.
2201
- suffix = f"{Colors.red}<TOUT: {sprint_time(job_done_run_time)}>"
2232
+ suffix = (
2233
+ f"<TOUT{safe_emoji(' ❌')}:"
2234
+ f" {sprint_time(job_done_run_time)}>"
2235
+ )
2236
+ this_job_failed = True
2202
2237
  else:
2203
- suffix = f"{Colors.red}<FAIL: {sprint_time(job_done_run_time)}>"
2238
+ suffix = (
2239
+ f"<FAIL{safe_emoji(' ❌')}:"
2240
+ f" {sprint_time(job_done_run_time)}>"
2241
+ )
2242
+ this_job_failed = True
2204
2243
  failed_jobs.append(job_done_name)
2205
2244
  else:
2206
- suffix = f"{Colors.green}<PASS: {sprint_time(job_done_run_time)}>"
2245
+ suffix = (
2246
+ f"<PASS{safe_emoji(' ✅')}:"
2247
+ f" {sprint_time(job_done_run_time)}>"
2248
+ )
2207
2249
  passed_jobs.append(job_done_name)
2208
2250
  # we want to print in one shot, because in fancy modes that's all that we're allowed
2209
2251
  job_done_text = "" if job_done_quiet else sprint_job_line(job_done_number,
2210
2252
  job_done_name)
2211
- if failed_jobs:
2212
- util.print_orange(f"{job_done_text}{suffix}")
2213
- else:
2253
+ if this_job_failed:
2254
+ util.print_red(f"{job_done_text}{suffix}")
2255
+ elif failed_jobs:
2214
2256
  util.print_yellow(f"{job_done_text}{suffix}")
2257
+ else:
2258
+ util.print_green(f"{job_done_text}{suffix}")
2215
2259
  self.jobs_status[job_done_number-1] = job_done_return_code
2216
2260
 
2217
2261
  if not anything_done:
2218
2262
  time.sleep(0.25) # if nothing happens for an iteration, chill out a bit
2219
2263
 
2220
2264
  if total_jobs:
2221
- emoji = "< :) >" if (len(passed_jobs) == total_jobs) else "< :( >"
2222
- util.info(sprint_job_line(final=True, job_name="jobs passed") + emoji, start="")
2265
+ if len(passed_jobs) == total_jobs:
2266
+ emojitxt = safe_emoji('😀', ':)')
2267
+ else:
2268
+ emojitxt = safe_emoji('😦', ':(')
2269
+ util.info(sprint_job_line(final=True, job_name="jobs passed") + f"< {emojitxt} >",
2270
+ start="")
2223
2271
  else:
2224
- util.info("Parallel: <No jobs found>")
2272
+ util.info(f"Parallel: <{safe_emoji('❓ ')}No jobs found>")
2225
2273
  # Make sure all jobs have a set status:
2226
2274
  for i, rc in enumerate(self.jobs_status):
2227
2275
  if rc is None or not isinstance(rc, int):
@@ -15,6 +15,7 @@ import shutil
15
15
  import mergedeep
16
16
 
17
17
  from opencos import util
18
+ from opencos.util import safe_emoji
18
19
  from opencos.utils.markup_helpers import yaml_safe_load, yaml_safe_writer
19
20
 
20
21
  class Defaults:
@@ -231,7 +232,7 @@ def get_config_merged_with_defaults(config:dict) -> dict:
231
232
  def get_argparser() -> argparse.ArgumentParser:
232
233
  '''Returns an ArgumentParser, handles --config-yml=<filename> arg'''
233
234
  parser = argparse.ArgumentParser(
234
- prog='opencos eda config options', add_help=False, allow_abbrev=False
235
+ prog=f'{safe_emoji("🔎 ")}opencos eda config options', add_help=False, allow_abbrev=False
235
236
  )
236
237
  parser.add_argument('--config-yml', type=str, default=Defaults.config_yml,
237
238
  help=('YAML filename to use for configuration (default'
@@ -94,6 +94,11 @@ file_extensions:
94
94
  dotf:
95
95
  - .f
96
96
  - .vc
97
+ python:
98
+ - .py
99
+ makefile:
100
+ - .mk
101
+
97
102
 
98
103
  inferred_top:
99
104
  # file extensions that we can infer "top" module from, if --top omitted.
@@ -332,7 +337,6 @@ tools:
332
337
  - "COCOTB_TEST_FAILED"
333
338
  log-must-strings:
334
339
  - "passed"
335
- - "Cocotb test completed successfully!"
336
340
 
337
341
 
338
342
  quartus:
@@ -42,12 +42,18 @@ def json_paths_to_jsonl(
42
42
  with open(output_json_path, 'w', encoding='utf-8') as outf:
43
43
 
44
44
  # jsonl is every line of the file is a json.
45
+ # We would expect the JSON to have "tests": [ ... ] with >= 1 test.
45
46
  for json_file_path in json_file_paths:
46
47
  with open(json_file_path, encoding='utf-8') as f:
47
48
  data = json.load(f)
48
49
  if len(assert_json_types) > 0 and type(data) not in assert_json_types:
49
50
  error(f'{json_file_path=} JSON data is not a Table (py dict) {type(data)=}')
50
- json.dump(data, outf)
51
+ if 'tests' in data and isinstance(data['tests'], list):
52
+ for test in data['tests']:
53
+ json.dump(test, outf)
54
+ else:
55
+ json.dump(data, outf)
56
+
51
57
  outf.write('\n')
52
58
  info(f'Wrote {len(json_file_paths)} tests to {output_json_path=}')
53
59
 
@@ -78,7 +84,12 @@ def json_paths_to_single_json(
78
84
  data = json.load(f)
79
85
  if len(assert_json_types) > 0 and type(data) not in assert_json_types:
80
86
  error(f'{json_file_path=} JSON data is not a Table (py dict) {type(data)=}')
81
- out_json_data['tests'].append(data)
87
+ if 'tests' in data and isinstance(data['tests'], list):
88
+ for test in data['tests']:
89
+ out_json_data['tests'].append(test)
90
+ else:
91
+ out_json_data['tests'].append(data)
92
+
82
93
  json.dump(out_json_data, outf)
83
94
  outf.write('\n')
84
95
  info(f'Wrote {len(json_file_paths)} tests {output_json_path=}')
@@ -471,7 +482,52 @@ class ExportHelper:
471
482
  yaml_safe_writer(data=data, filepath=dst)
472
483
 
473
484
 
474
- def create_export_json_in_out_dir( # pylint: disable=unused-argument
485
+ def create_combined_env_file_in_out_dir(self) -> list:
486
+ ''' Returns list of all .env lines to be put in output directory
487
+
488
+ Creates single exported .env file if any --env-file(s)
489
+ were loaded by CLI or DEPS targets. --input-file/-f was handled as args
490
+ already, but --env-files need to be special cased. We do not add this
491
+ combined ".env" file to the all_files list, just build it on the fly if needed.
492
+ '''
493
+ env_lines = []
494
+ for filepath in util.env_files_loaded:
495
+ with open(filepath, encoding='utf-8') as f:
496
+ env_lines.extend(f.readlines() + ['\n'])
497
+
498
+ if env_lines:
499
+ dst = os.path.join(self.out_dir, '.env')
500
+
501
+ # Write this to the export directory too:
502
+ with open(dst, 'w', encoding='utf-8') as f:
503
+ for lineno, line in enumerate(env_lines):
504
+
505
+ # modify VERILOG_SOURCES line if present, although it is known via DEPS.yml,
506
+ # we can set it according to files_v and files_sv:
507
+ if line.strip().startswith('VERILOG_SOURCES'):
508
+ base_verilog_filenames = [
509
+ os.path.split(x)[1] for x in \
510
+ self.cmd_design_obj.files_v + self.cmd_design_obj.files_sv
511
+ ]
512
+ env_lines[lineno] = (
513
+ f'VERILOG_SOURCES = {" ".join(base_verilog_filenames)}'
514
+ )
515
+ continue
516
+
517
+ # remove PYTHONPATH from .env
518
+ if line.strip().startswith('PYTHONPATH'):
519
+ env_lines[lineno] = ''
520
+ continue
521
+
522
+ f.write(line)
523
+
524
+
525
+ info(f'export_helper: Wrote {dst}')
526
+
527
+ return env_lines
528
+
529
+
530
+ def create_export_json_in_out_dir( # pylint: disable=unused-argument,too-many-locals,too-many-branches
475
531
  self, eda_config:dict={}, **kwargs
476
532
  ) -> None:
477
533
  '''Optionally creates an exported JSON file in the output directory'''
@@ -482,40 +538,43 @@ class ExportHelper:
482
538
  # assumes we've run self.create_deps_yml_in_out_dir():
483
539
  assert self.target
484
540
  assert self.out_deps_file
541
+ full_eda_cmd_list = ['eda', self.eda_command]
542
+ if self.args.get('waves', False):
543
+ full_eda_cmd_list.append('--waves')
544
+ if self.args.get('tool', ''):
545
+ full_eda_cmd_list.append(f'--tool={self.args["tool"]}')
546
+ full_eda_cmd_list.append(self.target)
485
547
 
486
548
  data = {
487
- 'name': self.target,
488
- 'eda': {
489
- 'enable': True,
490
- 'multi': False, # Not yet implemented.
491
- 'command': self.eda_command,
492
- 'targets': [self.target],
493
- 'args': [],
494
- 'waves': self.args.get('waves', False),
495
- # tool - eda.CommandSimVerilator has this set in self.args:
496
- 'tool': self.args.get('tool', None),
497
- },
498
- 'files': [],
549
+ 'correlationId': self.target,
550
+ 'jobType': 'edaCmd',
551
+ 'cmd': ' '.join(full_eda_cmd_list),
552
+ 'timeout': 600,
553
+ 'filesList': [], # filename (str), content (str)
499
554
  }
500
555
 
501
- # allow caller to override eda - tool, or eda - args, etc.
502
- for k,v in eda_config.items():
503
- if k in data['eda'] and v is not None:
504
- data['eda'][k] = v
505
-
506
556
  # Note that args may already be set via:
507
557
  # create_deps_yml_in_out_dir(deps_file_args=some_list)
508
558
  # For example, eda.CommandSim.do_export() will set certain allow-listed
509
559
  # args if present with non-default values.
510
560
 
511
-
512
561
  all_files = [self.out_deps_file] + self.included_files \
513
562
  + self.cmd_design_obj.files_sv + self.cmd_design_obj.files_v \
514
563
  + self.cmd_design_obj.files_vhd + self.cmd_design_obj.files_cpp \
564
+ + self.cmd_design_obj.files_sdc + self.cmd_design_obj.files_py \
565
+ + self.cmd_design_obj.files_makefile + self.cmd_design_obj.files_non_source
566
+
567
+ all_files = list(dict.fromkeys(all_files)) # uniqify list.
568
+
569
+ # The last file we handle is a single exported .env file if any --env-file(s)
570
+ env_lines = self.create_combined_env_file_in_out_dir()
571
+ if env_lines:
572
+ # write the updated env_lines to the export.json file.
573
+ data['filesList'].append({
574
+ 'filename': '.env',
575
+ 'content': ''.join(env_lines), # already has \n per line
576
+ })
515
577
 
516
- for x in self.cmd_design_obj.files_non_source:
517
- if x not in all_files:
518
- all_files.append(x)
519
578
 
520
579
  for somefile in all_files:
521
580
 
@@ -532,18 +591,17 @@ class ExportHelper:
532
591
 
533
592
  assert os.path.exists(somefile)
534
593
  with open(somefile, encoding='utf-8') as f:
535
- filestr = ''.join(f.readlines())
536
- data['files'].append({
537
- 'name': os.path.split(somefile)[1],
538
- 'content': filestr,
594
+ data['filesList'].append({
595
+ 'filename': os.path.split(somefile)[1],
596
+ 'content': ''.join(f.readlines()),
539
597
  })
540
598
 
541
-
599
+ test_runner_data = {'tests': [data]} # single test for test runner.
542
600
  dst = os.path.join(self.out_dir, 'export.json')
543
601
  with open(dst, 'w', encoding='utf-8') as f:
544
- json.dump(data, f)
602
+ json.dump(test_runner_data, f)
545
603
  f.write('\n')
546
- info(f'export_helper: Wrote {dst=}')
604
+ info(f'export_helper: Wrote {dst}')
547
605
 
548
606
  # If this was from an `export` command, and the self.out_dir != self.args['work-dir'], then
549
607
  # copy the export.json to the work-dir:
@@ -24,7 +24,9 @@ FORCE_PREFIX_DICT = {
24
24
  'vhdl@': 'vhdl',
25
25
  'cpp@': 'cpp',
26
26
  'sdc@': 'synth_constraints',
27
- 'f@': 'dotf'
27
+ 'f@': 'dotf',
28
+ 'py@' : 'python',
29
+ 'makefile@': 'makefile'
28
30
  }
29
31
 
30
32
  ALL_FORCED_PREFIXES = set(list(FORCE_PREFIX_DICT.keys()))