opencos-eda 0.3.6__tar.gz → 0.3.7__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.6/opencos_eda.egg-info → opencos_eda-0.3.7}/PKG-INFO +6 -4
  2. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/flist.py +2 -2
  3. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/multi.py +4 -3
  4. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/sim.py +12 -3
  5. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda.py +22 -20
  6. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_base.py +150 -64
  7. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/deps_files/command_order/DEPS.yml +13 -0
  8. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/helpers.py +9 -5
  9. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_eda.py +1 -1
  10. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_tools.py +9 -3
  11. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/verilator.py +1 -2
  12. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/yosys.py +3 -3
  13. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/util.py +27 -5
  14. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/utils/subprocess_helpers.py +23 -6
  15. {opencos_eda-0.3.6 → opencos_eda-0.3.7/opencos_eda.egg-info}/PKG-INFO +6 -4
  16. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos_eda.egg-info/requires.txt +3 -0
  17. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/pyproject.toml +9 -5
  18. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/LICENSE +0 -0
  19. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/LICENSE.spdx +0 -0
  20. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/README.md +0 -0
  21. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/__init__.py +0 -0
  22. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/_version.py +0 -0
  23. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/_waves_pkg.sv +0 -0
  24. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/__init__.py +0 -0
  25. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/build.py +0 -0
  26. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/deps_help.py +0 -0
  27. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/elab.py +0 -0
  28. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/export.py +0 -0
  29. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/lec.py +0 -0
  30. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/lint.py +0 -0
  31. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/open.py +0 -0
  32. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/proj.py +0 -0
  33. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/shell.py +0 -0
  34. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/sweep.py +0 -0
  35. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/synth.py +0 -0
  36. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/targets.py +0 -0
  37. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/upload.py +0 -0
  38. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/commands/waves.py +0 -0
  39. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/deps/__init__.py +0 -0
  40. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/deps/defaults.py +0 -0
  41. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/deps/deps_commands.py +0 -0
  42. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/deps/deps_file.py +0 -0
  43. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/deps/deps_processor.py +0 -0
  44. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/deps_schema.py +0 -0
  45. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_config.py +0 -0
  46. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_config_defaults.yml +0 -0
  47. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_config_max_verilator_waivers.yml +0 -0
  48. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_config_reduced.yml +0 -0
  49. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_deps_bash_completion.bash +0 -0
  50. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_deps_sanitize.py +0 -0
  51. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_extract_targets.py +0 -0
  52. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/eda_tool_helper.py +0 -0
  53. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/export_helper.py +0 -0
  54. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/export_json_convert.py +0 -0
  55. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/files.py +0 -0
  56. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/hw/__init__.py +0 -0
  57. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/hw/oc_cli.py +0 -0
  58. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/hw/pcie.py +0 -0
  59. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/names.py +0 -0
  60. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/peakrdl_cleanup.py +0 -0
  61. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/seed.py +0 -0
  62. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/__init__.py +0 -0
  63. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/custom_config.yml +0 -0
  64. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/deps_files/error_msgs/DEPS.yml +0 -0
  65. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -0
  66. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  67. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -0
  68. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -0
  69. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -0
  70. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_build.py +0 -0
  71. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_deps_helpers.py +0 -0
  72. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_deps_schema.py +0 -0
  73. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_eda_elab.py +0 -0
  74. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_eda_synth.py +0 -0
  75. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tests/test_oc_cli.py +0 -0
  76. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/__init__.py +0 -0
  77. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/cocotb.py +0 -0
  78. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/invio.py +0 -0
  79. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/invio_helpers.py +0 -0
  80. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/invio_yosys.py +0 -0
  81. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/iverilog.py +0 -0
  82. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/modelsim_ase.py +0 -0
  83. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/quartus.py +0 -0
  84. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/questa.py +0 -0
  85. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/questa_fse.py +0 -0
  86. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/riviera.py +0 -0
  87. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/slang.py +0 -0
  88. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/slang_yosys.py +0 -0
  89. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/surelog.py +0 -0
  90. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/tabbycad_yosys.py +0 -0
  91. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/tools/vivado.py +0 -0
  92. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/utils/__init__.py +0 -0
  93. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/utils/markup_helpers.py +0 -0
  94. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/utils/status_constants.py +0 -0
  95. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/utils/str_helpers.py +0 -0
  96. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/utils/vscode_helper.py +0 -0
  97. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos/utils/vsim_helper.py +0 -0
  98. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos_eda.egg-info/SOURCES.txt +0 -0
  99. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos_eda.egg-info/dependency_links.txt +0 -0
  100. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos_eda.egg-info/entry_points.txt +0 -0
  101. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/opencos_eda.egg-info/top_level.txt +0 -0
  102. {opencos_eda-0.3.6 → opencos_eda-0.3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.3.6
3
+ Version: 0.3.7
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
@@ -17,11 +17,13 @@ Requires-Dist: toml>=0.10.2
17
17
  Requires-Dist: yamllint>=1.35.1
18
18
  Requires-Dist: PySerial>=3.5
19
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
23
20
  Provides-Extra: dev
24
21
  Requires-Dist: pylint>=3.0.0; extra == "dev"
22
+ Requires-Dist: pytest>=8.3.5; extra == "dev"
23
+ Provides-Extra: cocotb
24
+ Requires-Dist: cocotb>=2.0; extra == "cocotb"
25
+ Requires-Dist: pytest>=8.3.5; extra == "cocotb"
26
+ Requires-Dist: coverage>=7.6.1; extra == "cocotb"
25
27
  Provides-Extra: docs
26
28
  Requires-Dist: mkdocs; extra == "docs"
27
29
  Requires-Dist: mkdocs-material; extra == "docs"
@@ -48,7 +48,7 @@ class CommandFList(CommandDesign):
48
48
  'bracket-quote-path' : False,
49
49
  'single-quote-path' : False,
50
50
  'double-quote-path' : False,
51
- 'no-quote-path' : False,
51
+ 'quote-path' : True,
52
52
  'build-script' : "", # we don't want this to error either
53
53
 
54
54
  'print-to-stdout': False,
@@ -122,7 +122,7 @@ class CommandFList(CommandDesign):
122
122
 
123
123
  pq1 = ""
124
124
  pq2 = "" # pq = path quote
125
- if self.args['no-quote-path']:
125
+ if not self.args['quote-path']:
126
126
  pass # if we decide to make one of the below default, this will override
127
127
  elif self.args['bracket-quote-path']:
128
128
  pq1 = "{"
@@ -28,7 +28,7 @@ class CommandMulti(CommandParallel):
28
28
  'fake': False,
29
29
  'parallel': 1,
30
30
  'single-timeout': None,
31
- 'fail-if-no-targets': False,
31
+ 'fail-if-no-targets': True,
32
32
  'export-jsonl': False,
33
33
  'print-targets': False,
34
34
  }
@@ -325,8 +325,9 @@ class CommandMulti(CommandParallel):
325
325
  current_targets = len(self.targets)
326
326
  util.info(f"Multi: Expanded {target_globs} to {len(self.targets)} {command} targets")
327
327
 
328
- if parsed.fail_if_no_targets and len(self.targets) == 0:
329
- self.error(f'Multi: --fail-if-no-targets set, and {self.targets=}')
328
+ if self.args['fail-if-no-targets'] and not self.targets:
329
+ self.error(f'Multi: --fail-if-no-targets set, and {self.targets=}. Disable with',
330
+ '--no-fail-if-no-targets, or see: eda multi --help')
330
331
  if not all_multi_tools:
331
332
  possible_tools = self.all_handler_commands.get(command, [])
332
333
  self.error(f'Multi: no tools to run for {command=}, available tools: {possible_tools}')
@@ -59,11 +59,11 @@ class CommandSim(CommandDesign):
59
59
  self.args.update({
60
60
  "pre-sim-tcl": [],
61
61
  'compile-args': [],
62
+ 'compile-waivers': [],
62
63
  'elab-args': [],
63
64
  'sim-args': [],
64
65
  'sim-plusargs': [], # lists are handled by 'set_arg(k,v)' so they append.
65
66
  'sim-library': [],
66
- 'compile-waivers': [],
67
67
  'sim-waivers': [],
68
68
  'coverage': False,
69
69
  'waves': False,
@@ -78,7 +78,13 @@ class CommandSim(CommandDesign):
78
78
  'verilate-args': [],
79
79
  })
80
80
  self.args_help.update({
81
+ 'pre-sim-tcl': (
82
+ 'Tool dependent, TCL file to run prior to simulation (if tool supports TCL)'
83
+ ),
81
84
  'compile-args': 'args added to sim/elab "compile" step',
85
+ 'compile-waivers': (
86
+ 'simulation tool depenent waivers by name, applied at compilation step'
87
+ ),
82
88
  'coverage': 'attempt to run coverage steps on the compile/elab/simulation',
83
89
  'elab-args': 'args added to sim/elab "elaboration" step, if required by tool',
84
90
  'log-bad-strings': 'strings that if present in the log will fail the simulation',
@@ -90,6 +96,10 @@ class CommandSim(CommandDesign):
90
96
  'sim-args': 'args added to final "simulation" step',
91
97
  'sim-plusargs': ('"simulation" step run-time args passed to tool, these can also'
92
98
  ' be set using --sim-plusargs=name[=value], or simply +name[=value]'),
99
+ 'sim-library': 'named libraries added to simulation, tool dependent',
100
+ 'sim-waivers': (
101
+ 'simulation tool depenent waivers by name, applied at "simulation" step'
102
+ ),
93
103
  'stop-before-compile': ('Create work-dir sh scripts for compile/elab/simulate, but do'
94
104
  ' not run them.'),
95
105
  'stop-after-compile': 'Create work-dir sh scripts, but only run the compile step.',
@@ -97,10 +107,9 @@ class CommandSim(CommandDesign):
97
107
  ' simulation step.'),
98
108
  'top': 'Name of topmost Verilog/SystemVerilog module, or VHDL entity',
99
109
  'verilate-args': ('args added to "compile" step in Verilator simulation'
100
- ' (for --tool=verilator)'),
110
+ ' (for --tool=verilator) these are identical to --compile-args'),
101
111
  'waves': 'Include waveforms, if possible for tool',
102
112
  'waves-start': 'Starting time of waveform capture, if possible for tool',
103
- 'work-dir': 'Optional override for working directory, defaults to ./eda.work/<top>.sim',
104
113
  'test-mode': ('stops the command early without executing, if --gui is present will'
105
114
  ' instead test without spawning gui')
106
115
 
@@ -20,8 +20,8 @@ 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
24
- from opencos.eda_base import Tool, which_tool, get_eda_exec
23
+ from opencos.util import safe_emoji, Colors
24
+ from opencos.eda_base import Tool, which_tool, get_eda_exec, print_eda_usage_line
25
25
  from opencos.utils import vsim_helper, vscode_helper
26
26
  from opencos.utils.subprocess_helpers import subprocess_run_background
27
27
  from opencos.utils import status_constants, str_helpers, subprocess_helpers
@@ -76,7 +76,7 @@ def get_all_commands_help_str(config: dict) -> str:
76
76
  all_commands_help.append(f' {key:<{max_command_str_len}} - {value.strip()}')
77
77
  if all_commands_help:
78
78
  all_commands_help = [
79
- 'Where <command> is one of:',
79
+ f'Where {Colors.byellow}COMMAND{Colors.normal} is one of:',
80
80
  '',
81
81
  ] + all_commands_help
82
82
  return '\n'.join(all_commands_help)
@@ -95,22 +95,24 @@ def usage(tokens: list, config: dict, command: str = "", tool: str = "") -> int:
95
95
  '''
96
96
 
97
97
  if command == "":
98
- print(
99
- f"""
100
- {safe_emoji("🔦 ")}Usage:
101
- eda [<options>] <command> [options] <files|targets, ...>
102
- """
103
- )
98
+ util.info('Help:', color=Colors.cyan)
99
+ print()
100
+ print_eda_usage_line()
104
101
  print(get_all_commands_help_str(config=config))
105
- print(
106
- f"""
107
- {safe_emoji("❕ ")}where <files|targets, ...> is one or more source file or DEPS markup file target,
108
- such as .v, .sv, .vhd[l], .cpp files, or a target key in a DEPS.[yml|yaml|toml|json].
109
- Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` to
110
- force use that file as systemverilog, verilog, vhdl, or C++, respectively.
111
-
112
- """
113
- )
102
+ print()
103
+ print(str_helpers.indent_wrap_long_text(
104
+ (
105
+ f'{safe_emoji("❕ ")}where {Colors.byellow}FILES|TARGETS{Colors.normal}'
106
+ ' is one or more source file or DEPS markup file target,'
107
+ ' such as .v, .sv, .vhd[l], .cpp files, or a target key in a'
108
+ f' {Colors.byellow}DEPS.[yml|yaml|toml|json]{Colors.normal}. Note that you can'
109
+ f' prefix source files with {Colors.bcyan}sv@{Colors.normal},'
110
+ f' {Colors.bcyan}v@{Colors.normal}, {Colors.bcyan}vhdl@{Colors.normal},'
111
+ f' or {Colors.bcyan}cpp@{Colors.normal}'
112
+ ' to force use that file as systemverilog, verilog, vhdl, or C++, respectively.'
113
+ ), width=str_helpers.get_terminal_columns(), indent=4
114
+ ))
115
+ print()
114
116
  eda_base.print_base_help()
115
117
  return 0
116
118
 
@@ -527,7 +529,7 @@ def main(*args):
527
529
  sys.exit(0)
528
530
 
529
531
  if not util.args['quiet']:
530
- util.info(f'eda: version {opencos.__version__}')
532
+ util.info(f'eda: version {opencos.__version__}', color=Colors.bcyan)
531
533
  # And show the command that was run (all args):
532
534
  util.info(f"main: eda {' '.join(args)}; (run from {os.getcwd()})")
533
535
 
@@ -537,7 +539,7 @@ def main(*args):
537
539
  # Note - we used to call: config = init_config(config=config)
538
540
  # However, we now defer calling init_config(..) until eda.process_tokens(..)
539
541
 
540
- util.info("*** OpenCOS EDA ***")
542
+ util.info("*** OpenCOS EDA ***", color=Colors.bcyan)
541
543
 
542
544
  if len(args) == 0 or (len(args) == 1 and '--debug' in args):
543
545
  # special snowflake case if someone called with a singular arg --debug
@@ -26,7 +26,7 @@ from opencos import eda_config
26
26
 
27
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
- indent_wrap_long_text, pretty_list_columns_manual
29
+ indent_wrap_long_text, pretty_list_columns_manual, get_terminal_columns
30
30
  from opencos.utils.subprocess_helpers import subprocess_run_background
31
31
  from opencos.utils import status_constants
32
32
 
@@ -42,6 +42,24 @@ def print_base_help() -> None:
42
42
  print(get_argparser_short_help())
43
43
 
44
44
 
45
+ def print_eda_usage_line(no_targets: bool = False, command_name='COMMAND') -> None:
46
+ '''Prints line for eda [options] COMMAND [options] FILES|TARGETS,...'''
47
+ print(f'{safe_emoji("🔦 ")}Usage:')
48
+ if no_targets:
49
+ print(
50
+ (f' {Colors.bold}{Colors.yellow}eda {Colors.cyan}[options]'
51
+ f' {Colors.yellow}{command_name} {Colors.cyan}[options]{Colors.normal}')
52
+ )
53
+ else:
54
+ print(
55
+ (f' {Colors.bold}{Colors.yellow}eda {Colors.cyan}[options]'
56
+ f' {Colors.yellow}{command_name} {Colors.cyan}[options]'
57
+ f' {Colors.yellow}FILES|TARGETS,...{Colors.normal}')
58
+ )
59
+ print()
60
+
61
+
62
+
45
63
  def get_argparser() -> argparse.ArgumentParser:
46
64
  '''Returns the ArgumentParser for general eda CLI'''
47
65
  parser = argparse.ArgumentParser(
@@ -82,7 +100,7 @@ def get_argparsers_args_list() -> list:
82
100
 
83
101
 
84
102
  def get_eda_exec(command: str = '') -> str:
85
- '''Returns the full path of `eda` executable to be used for a given eda <command>'''
103
+ '''Returns the full path of `eda` executable to be used for a given eda COMMAND'''
86
104
  # NOTE(drew): This is kind of flaky. 'eda multi' reinvokes 'eda'. But the executable for 'eda'
87
105
  # is one of:
88
106
  # 1. pip3 install opencos-eda
@@ -133,6 +151,17 @@ def which_tool(command: str, config: dict) -> str:
133
151
  return tool
134
152
 
135
153
 
154
+ def get_class_tool_name(command_obj: object) -> str:
155
+ '''Attempts to return command_obj._TOOL via command_obj.get_tool_name()'''
156
+ if f := getattr(command_obj, 'get_tool_name', None):
157
+ if callable(f):
158
+ ret = f()
159
+ if ret and isinstance(ret, str):
160
+ return ret
161
+ return ''
162
+
163
+
164
+
136
165
  class Tool:
137
166
  '''opencos.eda_base.Tool is a base class used by opencos.tools.<name>.
138
167
 
@@ -182,6 +211,10 @@ class Tool:
182
211
  util.info(f'Override for {self._TOOL} using exe {exe}')
183
212
  self._EXE = exe
184
213
 
214
+ def get_tool_name(self) -> str:
215
+ '''Returns _TOOL'''
216
+ return str(self._TOOL)
217
+
185
218
  def get_full_tool_and_versions(self) -> str:
186
219
  '''Returns tool:version, such as: verilator:5.033'''
187
220
  if not self._VERSION:
@@ -198,7 +231,7 @@ class Tool:
198
231
 
199
232
 
200
233
  class Command: # pylint: disable=too-many-public-methods
201
- '''Base class for all: eda <command>
234
+ '''Base class for all: eda COMMAND
202
235
 
203
236
  The Command class should be used when you don't require files, otherwise consider
204
237
  CommandDesign.
@@ -235,21 +268,54 @@ class Command: # pylint: disable=too-many-public-methods
235
268
  'error-unknown-args': True,
236
269
  })
237
270
  self.args_help.update({
238
- 'stop-before-compile': ('stop this run before any compile (if possible for tool) and'
239
- ' save .sh scripts in eda-dir/'),
240
- 'eda-dir': 'relative directory where eda logs are saved',
271
+ 'keep': (
272
+ 'Determined by eda <tool>, generally prevents jobs from overwriting artifacts'
273
+ ),
274
+ 'force': 'Determined by eda <tool>, force override',
275
+ 'fake': (
276
+ 'Determined by eda <tool>, generally is a dry-run that does not execute tool'
277
+ ),
278
+ 'stop-before-compile': (
279
+ 'stop this run before any compile steps (if possible for tool) and'
280
+ ' save .sh scripts in eda-dir/'
281
+ ),
282
+ 'stop-after-compile': (
283
+ 'stop this run after any compile steps (if possible for tool) and'
284
+ ' save .sh scripts in eda-dir/'
285
+ ),
286
+ 'stop-after-elaborate': (
287
+ 'stop this run after any elaborate steps (if possible for tool) and'
288
+ ' save .sh scripts in eda-dir/'
289
+ ),
290
+ 'lint': 'tool dependent, run with linting options',
291
+ "job-name" : 'Optional, used to create a sub directory under base work-dir (eda-dir)',
292
+ "sub-work-dir" : (
293
+ 'Optional, similar to job-name, can be used to name the directory created under'
294
+ ' eda-dir'
295
+ ),
296
+ 'eda-dir': 'Optional, relative base directory where eda logs are saved',
241
297
  'export': 'export results for these targets in eda-dir',
242
298
  'export-run': 'export, and run, results for these targets in eda-dir',
243
299
  'export-json': 'export, and save a JSON file per target',
244
- 'work-dir': ('Optional override for working directory, often defaults to'
245
- ' ./eda.work/<top>.<command>'),
300
+ 'work-dir': ('Optional override for working directory, if unset will often use '
301
+ ' ./eda.work/[TOP|TARGET].COMMAND'),
246
302
  "work-dir-use-target-dir": ('Set the work-dir to be the same as the in-place location'
247
303
  ' where the target (DEPS) exists'),
248
- 'enable-tags': ('DEPS markup tag names to be force enabled for this'
249
- ' command (mulitple appends to list).'),
250
- 'diable-tags': ('DEPS markup tag names to be disabled (even if they'
251
- ' match the criteria) for this command (mulitple appends to list).'
252
- ' --disable-tags has higher precedence than --enable-tags.'),
304
+ "suffix": (
305
+ "Optional, determined by eda COMMAND, used by 'multi' jobs information logging"
306
+ ),
307
+ "design": (
308
+ "Optional, used to override both the work-dir, and if unset the top DEPS target"
309
+ ),
310
+ 'enable-tags': (
311
+ 'DEPS markup tag names to be force enabled for this'
312
+ ' command (mulitple appends to list).'
313
+ ),
314
+ 'disable-tags': (
315
+ 'DEPS markup tag names to be disabled (even if they match the criteria) for this'
316
+ ' command (mulitple appends to list). --disable-tags has higher precedence than'
317
+ ' --enable-tags.'
318
+ ),
253
319
  'test-mode': ('command and tool dependent, usually stops the command early without'
254
320
  ' executing.'),
255
321
  'error-unknown-args': (
@@ -356,7 +422,7 @@ class Command: # pylint: disable=too-many-public-methods
356
422
  ) -> str:
357
423
  '''Creates the working directory and populates self.args['work-dir']
358
424
 
359
- Generally uses ./ self.args['eda-dir'] / <target-name>.<command> /
425
+ Generally uses ./ self.args['eda-dir'] / TARGET-NAME.COMMAND /
360
426
  however, self.args['job-name'] or ['sub-work-dir'] can override that.
361
427
 
362
428
  Additionally, the work-dir is attempted to be deleted if it already exists
@@ -619,20 +685,33 @@ class Command: # pylint: disable=too-many-public-methods
619
685
  # is []. If the parsed Namespace has values set to None or [], we do not update. This
620
686
  # means that as deps are processed that have args set, they cannot override the top
621
687
  # level args that were already set, nor be overriden by defaults.
622
- if isinstance(value, bool):
623
- # For bool, support --key and --no-key with action=argparse.BooleanOptionalAction.
624
- # Note, this means you cannot use --some-bool=True, or --some-bool=False, has to
625
- # be --some-bool or --no-some-bool.
626
- parser.add_argument(
627
- *arguments, default=None, **bool_action_kwargs, **help_kwargs)
628
- elif isinstance(value, (list, set)):
629
- parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
630
- elif isinstance(value, (int, str)):
631
- parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
632
- elif value is None:
633
- parser.add_argument(*arguments, default=None, **help_kwargs)
634
- else:
635
- assert False, f'{key=} {value=} how do we do argparse for this type of value?'
688
+ try:
689
+ if isinstance(value, bool):
690
+ # For bool, support --key and --no-key with
691
+ # action=argparse.BooleanOptionalAction. Note, this means you cannot use
692
+ # --some-bool=True, or --some-bool=False, has to be --some-bool or
693
+ # --no-some-bool.
694
+ # Also we cannot have self.args.keys() that start with 'no-', which is
695
+ # why this entire thing is wrapped with try/except.
696
+ parser.add_argument(
697
+ *arguments, default=None, **bool_action_kwargs, **help_kwargs)
698
+ elif isinstance(value, (list, set)):
699
+ parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
700
+ elif isinstance(value, (int, str)):
701
+ parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
702
+ elif value is None:
703
+ parser.add_argument(*arguments, default=None, **help_kwargs)
704
+ else:
705
+ assert False, f'{key=} {value=} how do we do argparse for this type of value?'
706
+ except Exception as e:
707
+ if isinstance(value, bool):
708
+ self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
709
+ f'{arguments=} {bool_action_kwargs=} {help_kwargs=} {e=}')
710
+ else:
711
+ self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
712
+ f'{arguments=} {help_kwargs=} {e=}')
713
+
714
+
636
715
 
637
716
  return parser
638
717
 
@@ -730,7 +809,7 @@ class Command: # pylint: disable=too-many-public-methods
730
809
  def get_command_from_unparsed_args(
731
810
  self, tokens: list, error_if_no_command: bool = True
732
811
  ) -> str:
733
- '''Given a list of unparsed args, try to fish out the eda <command> value.
812
+ '''Given a list of unparsed args, try to fish out the eda COMMAND value.
734
813
 
735
814
  This will remove the value from the tokens list.
736
815
  '''
@@ -742,7 +821,7 @@ class Command: # pylint: disable=too-many-public-methods
742
821
  break
743
822
 
744
823
  if not ret and error_if_no_command:
745
- self.error(f"Looking for a valid eda {self.command_name} <command>",
824
+ self.error(f"Looking for a valid eda {self.command_name} COMMAND",
746
825
  f"but didn't find one in {tokens=}",
747
826
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
748
827
  return ret
@@ -808,19 +887,16 @@ class Command: # pylint: disable=too-many-public-methods
808
887
  if self.args_help has additional help information.
809
888
  '''
810
889
 
811
- # Indent long lines (>100) to indent=56 (this is where we leave off w/ {vstr:12} below.
812
- def indent_me(text:str):
813
- return indent_wrap_long_text(text, width=100, indent=56)
890
+ # Indent long lines (>100) to indent=24 (this is where usual argparse help leaves off)
891
+ def indent_me(text: str, initial_indent: int = 23):
892
+ return indent_wrap_long_text(
893
+ ' ' * initial_indent + text, width=get_terminal_columns(), indent=24
894
+ )
814
895
 
815
- util.info('Help:')
896
+ util.info('Help:', color=Colors.cyan)
816
897
  # using bare 'print' here, since help was requested, avoids --color and --quiet
817
898
  print()
818
- print(f'{safe_emoji("🔦 ")}Usage:')
819
- if no_targets:
820
- print(f' eda [options] {self.command_name} [options]')
821
- else:
822
- print(f' eda [options] {self.command_name} [options] [files|targets, ...]')
823
- print()
899
+ print_eda_usage_line(no_targets=no_targets, command_name=self.command_name)
824
900
 
825
901
  print_base_help()
826
902
  lines = []
@@ -829,10 +905,18 @@ class Command: # pylint: disable=too-many-public-methods
829
905
  return
830
906
 
831
907
  if self.command_name:
832
- lines.append(f"{safe_emoji('🔧 ')}Generic help for command='{self.command_name}'"
833
- f" (using '{self.__class__.__name__}')")
908
+ line = (
909
+ f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help for"
910
+ f" command={Colors.byellow}{self.command_name}{Colors.cyan}"
911
+ )
912
+ if tool := get_class_tool_name(self):
913
+ line += f" tool={Colors.byellow}{tool}"
914
+ line += f" {Colors.normal}(using '{self.__class__.__name__}')"
915
+ lines.append(line)
834
916
  else:
835
- lines.append("{safe_emoji('🔧 ')}Generic help (from class Command):")
917
+ lines.append(
918
+ f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help:{Colors.normal}"
919
+ )
836
920
 
837
921
  # Attempt to run argparser on args, but don't error if it fails.
838
922
  unparsed = []
@@ -842,44 +926,46 @@ class Command: # pylint: disable=too-many-public-methods
842
926
  except Exception:
843
927
  pass
844
928
 
845
- for k in sorted(self.args.keys()):
846
- v = self.args[k]
847
- vstr = str(v)
848
- khelp = self.args_help.get(k, '')
849
- if khelp:
850
- khelp = f' - {khelp}'
851
- if isinstance(v, bool):
852
- lines.append(indent_me(f" --{k:20} : boolean : {vstr:12}{khelp}"))
853
- elif isinstance(v, int):
854
- lines.append(indent_me(f" --{k:20} : integer : {vstr:12}{khelp}"))
855
- elif isinstance(v, list):
856
- lines.append(indent_me(f" --{k:20} : list : {vstr:12}{khelp}"))
857
- elif isinstance(v, str):
858
- vstr = "'" + v + "'"
859
- lines.append(indent_me(f" --{k:20} : string : {vstr:12}{khelp}"))
860
- else:
861
- lines.append(indent_me(f" --{k:20} : <unknown> : {vstr:12}{khelp}"))
929
+ short_help_lines = util.get_argparser_short_help(
930
+ parser=self.get_argparser(support_underscores=False)
931
+ ).split('\n')
932
+ short_help_lines.pop(0) # Strip first line w/ argparser prog name
933
+ lines.extend(short_help_lines)
934
+
935
+ # For these custom args, -GParameter=Value, +define+Name[=value] +incdir+path
936
+ # make colors to look like python >= 3.14, if that is our version.
937
+ color = Colors()
938
+ if sys.version_info.major < 3 or \
939
+ (sys.version_info.major == 3 and sys.version_info.minor < 14):
940
+ color.disable() # strip our color object if < 3.14
941
+
862
942
 
863
943
  lines.append('')
944
+ lines.append(
945
+ f" {color.cyan}-G{color.byellow}<parameterName>{color.normal}=" \
946
+ + f"{color.yellow}<value>{color.normal}"
947
+ )
864
948
  lines.append(indent_me((
865
- " -G<parameterName>=<value> "
866
949
  " Add parameter to top level, support bit/int/string types only."
867
950
  " Example: -GDEPTH=8 (DEPTH treated as SV int/integer)."
868
951
  " -GENABLE=1 (ENABLED treated as SV bit/int/integer)."
869
952
  " -GName=eda (Name treated as SV string \"eda\")."
870
953
  )))
954
+
955
+ lines.append(f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}")
871
956
  lines.append(indent_me((
872
- " +define+<defineName> "
873
957
  " Add define w/out value to tool ahead of SV sources"
874
958
  " Example: +define+SIM_SPEEDUP"
875
959
  )))
960
+ lines.append(
961
+ f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}=" \
962
+ + f"{color.yellow}<value>{color.normal}")
876
963
  lines.append(indent_me((
877
- " +define+<defineName>=<value> "
878
964
  " Add define w/ value to tool ahead of SV sources"
879
965
  " Example: +define+TECH_LIB=2 +define+FULL_NAME=\"E D A\""
880
966
  )))
967
+ lines.append(f" {color.cyan}+incdir+{color.byellow}PATH{color.normal}")
881
968
  lines.append(indent_me((
882
- " +incdir+<path> "
883
969
  " Add path (absolute or relative) for include directories"
884
970
  " for SystemVerilog `include \"<some-file>.svh\""
885
971
  " Example: +incdir+../lib"
@@ -29,3 +29,16 @@ target_test_with_post_tool_commands:
29
29
  run-after-tool: true
30
30
  - target_echo_hi_bye
31
31
  top: foo
32
+
33
+
34
+ test_run_from_work_dir:
35
+ deps:
36
+ - commands:
37
+ - shell: echo "pwd=$PWD"
38
+ run-from-work-dir: true
39
+
40
+ test_run_from_work_dir_false:
41
+ deps:
42
+ - commands:
43
+ - shell: echo "pwd=$PWD"
44
+ run-from-work-dir: false
@@ -10,8 +10,8 @@ from pathlib import Path
10
10
  from contextlib import redirect_stdout, redirect_stderr
11
11
 
12
12
  from opencos import eda, eda_tool_helper, deps_schema
13
- from opencos.utils.markup_helpers import yaml_safe_load
14
13
  from opencos.utils import status_constants
14
+ from opencos.utils.markup_helpers import yaml_safe_load
15
15
  from opencos.utils.subprocess_helpers import subprocess_run_background
16
16
 
17
17
  # Figure out what tools the system has available, without calling eda.main(..)
@@ -238,7 +238,7 @@ class Helpers:
238
238
 
239
239
  def log_it(
240
240
  self, command_str: str, logfile=None, use_eda_wrap: bool = True,
241
- run_in_subprocess: bool = False,
241
+ run_in_subprocess: bool = False, include_default_log: bool = False,
242
242
  preserve_env: bool = False
243
243
  ) -> int:
244
244
  '''Replacement for calling eda.main or eda_wrap, when you want an internal logfile
@@ -257,13 +257,17 @@ class Helpers:
257
257
  logfile = self._resolve_logfile(logfile)
258
258
  rc = 50
259
259
 
260
+ eda_log_arg = '--no-default-log'
261
+ if include_default_log:
262
+ eda_log_arg = ''
263
+
260
264
  # TODO(drew): There are some issues with log_it redirecting stdout from vivado
261
265
  # and modelsim_ase. So this may not work for all tools, you may have to directly
262
266
  # look at eda.work/{target}.sim/sim.log or xsim.log.
263
267
  print(f'{os.getcwd()=}')
264
268
  print(f'{command_str=}')
265
269
  if run_in_subprocess or self.RUN_IN_SUBPROCESS:
266
- command_list = ['eda', '--no-default-log'] + command_str.split()
270
+ command_list = ['eda', eda_log_arg] + command_str.split()
267
271
  _, _, rc = subprocess_run_background(
268
272
  work_dir=self.DEFAULT_DIR,
269
273
  command_list=command_list,
@@ -275,9 +279,9 @@ class Helpers:
275
279
  with open(logfile, 'w', encoding='utf-8') as f:
276
280
  with redirect_stdout(f), redirect_stderr(f):
277
281
  if use_eda_wrap or self.USE_EDA_WRAP:
278
- rc = eda_wrap('--no-default-log', *(command_str.split()))
282
+ rc = eda_wrap(eda_log_arg, *(command_str.split()))
279
283
  else:
280
- rc = eda.main('--no-default-log', *(command_str.split()))
284
+ rc = eda.main(eda_log_arg, *(command_str.split()))
281
285
  print(f'Wrote: {os.path.abspath(logfile)=}')
282
286
 
283
287
  if self.PRESERVE_ENV or preserve_env:
@@ -648,7 +648,7 @@ class TestsRequiresIVerilog(Helpers):
648
648
  print(f'{rc=}')
649
649
  assert rc == 0
650
650
  assert self.is_in_log('Detected iverilog')
651
- assert self.is_in_log("Generic help for command='sim' (using 'CommandSimIverilog')")
651
+ assert self.is_in_log("Generic help for command=sim tool=iverilog")
652
652
 
653
653
  def test_iverilog_sim(self):
654
654
  '''Test for command sim'''
@@ -3,6 +3,7 @@
3
3
  # pylint: disable=R0801 # (similar lines in 2+ files)
4
4
 
5
5
  import os
6
+ import shutil
6
7
  import sys
7
8
  import pytest
8
9
 
@@ -32,14 +33,19 @@ def test_tools_loaded():
32
33
  assert config
33
34
  assert len(config.keys()) > 0
34
35
 
36
+
35
37
  # It's possible we're running in some container or install that has no tools, for example,
36
38
  # Windows.
37
39
  if sys.platform.startswith('win') and \
38
40
  not helpers.can_run_eda_command('elab', 'sim', cfg=config):
39
- # Windows, not handlers for elab or sim:
41
+ # Windows, no handlers for elab or sim:
40
42
  pass
41
43
  else:
42
- assert len(tools_loaded) > 0
44
+ basic_tools_available_via_path = any(shutil.which(x) for x in (
45
+ 'verilator', 'iverilog', 'xsim', 'vsim', 'slang', 'yosys'
46
+ ))
47
+ if basic_tools_available_via_path:
48
+ assert len(tools_loaded) > 0
43
49
 
44
50
  def version_checker(
45
51
  obj: eda_base.Tool, chk_str: str
@@ -49,7 +55,7 @@ def test_tools_loaded():
49
55
  assert chk_str in full_ver, f'{chk_str=} not in {full_ver=}'
50
56
  ver_num = full_ver.rsplit(':', maxsplit=1)[-1]
51
57
  if 'b' in ver_num:
52
- ver_num = ver_num.split('b')[0] # TODO(chaitanya): remove once cocotb 2.0 is released
58
+ ver_num = ver_num.split('b')[0]
53
59
  if '.' in ver_num:
54
60
  major_ver = ver_num.split('.')[0]
55
61
  assert major_ver.isdigit(), (
@@ -76,7 +76,6 @@ class VerilatorSim(CommandSim, ToolVerilator):
76
76
  ToolVerilator.__init__(self, config=self.config)
77
77
  self.args.update({
78
78
  'gui': False,
79
- 'tcl-file': None,
80
79
  'dump-vcd': False,
81
80
  'waves-fst': True,
82
81
  'waves-vcd': False,
@@ -95,7 +94,7 @@ class VerilatorSim(CommandSim, ToolVerilator):
95
94
  ' plusarg and apply $dumpfile("dump.fst").'
96
95
  ),
97
96
  'waves-fst': (
98
- '(Default True) If using --waves, apply simulation runtime arg +trace.'
97
+ 'If using --waves, apply simulation runtime arg +trace.'
99
98
  ' Note that if you do not have SV code using $dumpfile, eda will add'
100
99
  ' _waves_pkg.sv to handle this for you with +trace runtime plusarg.'
101
100
  ),
@@ -70,9 +70,9 @@ class ToolYosys(Tool):
70
70
  [self.sta_exe, '-version'], capture_output=True, check=False
71
71
  )
72
72
  util.debug(f'{self.yosys_exe} {sta_version_ret=}')
73
- sta_ver = sta_version_ret.stdout.decode('utf-8', errors='replace').split()[0]
74
- if sta_ver:
75
- self.sta_version = sta_ver
73
+ sta_ver = sta_version_ret.stdout.decode('utf-8', errors='replace').split()
74
+ if sta_ver and isinstance(sta_ver, list):
75
+ self.sta_version = sta_ver[0]
76
76
 
77
77
  version_ret = subprocess.run(
78
78
  [self.yosys_exe, '--version'], capture_output=True, check=False
@@ -54,7 +54,11 @@ class Colors:
54
54
  red = "\x1B[31m"
55
55
  green = "\x1B[32m"
56
56
  yellow = "\x1B[33m" # This looks orange, but it's techincally yellow
57
+ cyan = "\x1b[36m"
57
58
  foreground = "\x1B[39m"
59
+ bold = "\x1B[1m"
60
+ byellow = "\x1B[1m\x1B[33m"
61
+ bcyan = "\x1B[1m\x1b[36m"
58
62
  normal = "\x1B[0m"
59
63
 
60
64
  @staticmethod
@@ -67,6 +71,14 @@ class Colors:
67
71
  return color + text + "\x1B[0m" # (normal)
68
72
  return text
69
73
 
74
+ def disable(self) -> None:
75
+ '''Clears all color str in class Colors, to prevent print() methods that use
76
+ util.Colors from printing color'''
77
+ for x in dir(self):
78
+ if not callable(getattr(self, x, None)) and isinstance(x, str) and \
79
+ not x.startswith('_'):
80
+ setattr(self, x, '')
81
+
70
82
 
71
83
  def safe_emoji(emoji: str, default: str = '') -> str:
72
84
  '''Returns emoji character if args['emoji'] is True'''
@@ -421,7 +433,7 @@ def get_argparser_short_help(parser: object = None) -> str:
421
433
  break
422
434
 
423
435
  # skip the line that says 'options:', replace with the progname:
424
- return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
436
+ return f'{Colors.cyan}{parser.prog}:{Colors.normal}\n' + '\n'.join(full_lines[lineno + 1:])
425
437
 
426
438
 
427
439
  def process_token(arg: list) -> bool:
@@ -533,13 +545,14 @@ def process_tokens( # pylint: disable=too-many-branches
533
545
  global debug_level # pylint: disable=global-statement
534
546
  debug_level = 0
535
547
 
536
- # Deal with --debug, --debug-level, --env-file, -f/--input-file(s) tokens first,
537
- # for now put dot-f file contents in front of of tokens, do this first in a
538
- # separate custom argparser:
548
+ # Deal with --version, --debug, --debug-level, --env-file, -f/--input-file(s)
549
+ # tokens first, for now put dot-f file contents in front of of tokens, do this first
550
+ # in a separate custom argparser:
539
551
  bool_action_kwargs = get_argparse_bool_action_kwargs()
540
552
  parser = argparse.ArgumentParser(
541
553
  prog='opencos -f/--input-file', add_help=False, allow_abbrev=False
542
554
  )
555
+ parser.add_argument('--version', default=False, action='store_true')
543
556
  parser.add_argument('--debug', **bool_action_kwargs,
544
557
  help='Display additional debug messaging level 1 or higher')
545
558
  parser.add_argument('--debug-level', type=int, default=0,
@@ -560,6 +573,10 @@ def process_tokens( # pylint: disable=too-many-branches
560
573
  except argparse.ArgumentError:
561
574
  error(f'util -f/--input-file, problem attempting to parse_known_args for: {tokens}')
562
575
 
576
+ if parsed.version:
577
+ # Stop processing, return nothing, caller can print version and exit.
578
+ return parsed, []
579
+
563
580
  process_debug_args(parsed=parsed)
564
581
  debug(f'util.process_tokens: {parsed=} {unparsed=} from: {tokens}')
565
582
 
@@ -599,6 +616,9 @@ def process_tokens( # pylint: disable=too-many-branches
599
616
  warning(f'python error, nested -f/--input-file(s) {parsed.input_file} should',
600
617
  'have already been resolved')
601
618
 
619
+ if not parsed.color:
620
+ Colors.disable(Colors) # strip strings in Colors class
621
+
602
622
  # clear existing artifacts dicts (mostly for pytests repeatedly calling eda.main),
603
623
  # set artifacts.enabled based on args['artifacts-json']
604
624
  artifacts.reset(enable=parsed.artifacts_json)
@@ -836,7 +856,9 @@ def error(
836
856
  global max_error_code # pylint: disable=global-statement
837
857
 
838
858
  if start is None:
839
- start = "ERROR: " + (f"[{progname}] " if progname_in_message else "")
859
+ start = f"{Colors.bold}ERROR:{Colors.normal}{Colors.red} "
860
+ if progname_in_message:
861
+ start += f"[{progname}] "
840
862
  start += safe_emoji("❌ ")
841
863
  args['errors'] += 1
842
864
  max_error_code = max(max_error_code, error_code)
@@ -6,7 +6,9 @@ import subprocess
6
6
  import sys
7
7
 
8
8
  import psutil
9
+ from opencos import util
9
10
  from opencos.util import debug, error, info, warning, progname, global_log
11
+ from opencos.utils.str_helpers import strip_ansi_color
10
12
 
11
13
  IS_WINDOWS = sys.platform.startswith('win')
12
14
 
@@ -68,7 +70,7 @@ def subprocess_run_background( # pylint: disable=too-many-branches
68
70
 
69
71
  proc_kwargs = {'shell': shell,
70
72
  'stdout': subprocess.PIPE,
71
- 'stderr': subprocess.STDOUT,
73
+ 'stderr': subprocess.STDOUT
72
74
  }
73
75
  if work_dir:
74
76
  proc_kwargs['cwd'] = work_dir
@@ -101,14 +103,29 @@ def subprocess_run_background( # pylint: disable=too-many-branches
101
103
  error(f'Unable to open file "{tee_fpath}" for writing, {e}')
102
104
 
103
105
  for line in iter(proc.stdout.readline, b''):
104
- line = line.rstrip().decode("utf-8", errors="replace")
106
+ line = line.decode("utf-8", errors="replace") # leave \n intact
107
+
108
+ # Since we don't control what the subprocess command did, if it
109
+ # thinks we support color, but user ran with --no-color, we need to strip ANSI colors:
110
+ if not util.args['color']:
111
+ line = strip_ansi_color(line)
112
+
113
+ # Print the line with color, if --color:
105
114
  if not background:
106
- print(line)
115
+ print(line, end='')
116
+
117
+ # for all logs, and the returned stdout str, if we haven't stripped color yet,
118
+ # we need to now, before writing to tee_fpath_f, or to global_log:
119
+ if util.args['color']:
120
+ line = strip_ansi_color(line)
121
+ line = line.replace('\r', '') # remove CR
122
+
107
123
  if tee_fpath_f:
108
- tee_fpath_f.write(line + '\n')
124
+ tee_fpath_f.write(line)
109
125
  if global_log.file:
110
- global_log.write(line, '\n')
111
- stdout += line + '\n'
126
+ # directly write to file handle, avoid util.UtilLogger.write(line, end='')
127
+ global_log.file.write(line)
128
+ stdout += line
112
129
 
113
130
  proc.communicate()
114
131
  remove_completed_parent_pid(proc.pid)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.3.6
3
+ Version: 0.3.7
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
@@ -17,11 +17,13 @@ Requires-Dist: toml>=0.10.2
17
17
  Requires-Dist: yamllint>=1.35.1
18
18
  Requires-Dist: PySerial>=3.5
19
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
23
20
  Provides-Extra: dev
24
21
  Requires-Dist: pylint>=3.0.0; extra == "dev"
22
+ Requires-Dist: pytest>=8.3.5; extra == "dev"
23
+ Provides-Extra: cocotb
24
+ Requires-Dist: cocotb>=2.0; extra == "cocotb"
25
+ Requires-Dist: pytest>=8.3.5; extra == "cocotb"
26
+ Requires-Dist: coverage>=7.6.1; extra == "cocotb"
25
27
  Provides-Extra: docs
26
28
  Requires-Dist: mkdocs; extra == "docs"
27
29
  Requires-Dist: mkdocs-material; extra == "docs"
@@ -8,12 +8,15 @@ toml>=0.10.2
8
8
  yamllint>=1.35.1
9
9
  PySerial>=3.5
10
10
  supports_color>=0.2.0
11
+
12
+ [cocotb]
11
13
  cocotb>=2.0
12
14
  pytest>=8.3.5
13
15
  coverage>=7.6.1
14
16
 
15
17
  [dev]
16
18
  pylint>=3.0.0
19
+ pytest>=8.3.5
17
20
 
18
21
  [docs]
19
22
  mkdocs
@@ -2,7 +2,7 @@
2
2
 
3
3
  [project]
4
4
  name = "opencos-eda"
5
- version = "0.3.6"
5
+ version = "0.3.7"
6
6
  dependencies = [
7
7
  # opencos/eda.py dependencies
8
8
  "mergedeep >= 1.3.4",
@@ -16,10 +16,6 @@ dependencies = [
16
16
  # opencos/oc_cli.py dependencies
17
17
  "PySerial >= 3.5",
18
18
  "supports_color >= 0.2.0",
19
- # cocotb, pytest + coverage no longer optional.
20
- "cocotb >= 2.0",
21
- "pytest >= 8.3.5",
22
- "coverage >= 7.6.1", # For python3.10, we could go >= 7.11.3
23
19
  ]
24
20
  requires-python = ">= 3.8"
25
21
  authors = [
@@ -31,6 +27,14 @@ description = "A simple Python package for wrapping RTL simuliatons and synthesi
31
27
  [project.optional-dependencies]
32
28
  dev = [
33
29
  "pylint >= 3.0.0",
30
+ "pytest >= 8.3.5",
31
+ ]
32
+ cocotb = [
33
+ # cocotb is optional. You may want to set env COCOTB_IGNORE_PYTHON_REQUIRES
34
+ # cocotb set a maximum python version
35
+ "cocotb >= 2.0",
36
+ "pytest >= 8.3.5",
37
+ "coverage >= 7.6.1", # For python3.10, we could go >= 7.11.3
34
38
  ]
35
39
  docs = [
36
40
  "mkdocs",
File without changes
File without changes
File without changes
File without changes
File without changes