opencos-eda 0.3.10__py3-none-any.whl → 0.3.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. opencos/commands/deps_help.py +63 -113
  2. opencos/commands/export.py +7 -2
  3. opencos/commands/multi.py +4 -4
  4. opencos/commands/sim.py +14 -15
  5. opencos/commands/sweep.py +1 -1
  6. opencos/commands/synth.py +1 -2
  7. opencos/commands/upload.py +192 -4
  8. opencos/commands/waves.py +52 -8
  9. opencos/deps/deps_commands.py +6 -6
  10. opencos/deps/deps_processor.py +129 -50
  11. opencos/docs/Architecture.md +45 -0
  12. opencos/docs/ConnectingApps.md +29 -0
  13. opencos/docs/DEPS.md +199 -0
  14. opencos/docs/Debug.md +77 -0
  15. opencos/docs/DirectoryStructure.md +22 -0
  16. opencos/docs/Installation.md +117 -0
  17. opencos/docs/OcVivadoTcl.md +63 -0
  18. opencos/docs/OpenQuestions.md +7 -0
  19. opencos/docs/README.md +13 -0
  20. opencos/docs/RtlCodingStyle.md +54 -0
  21. opencos/docs/eda.md +147 -0
  22. opencos/docs/oc_cli.md +135 -0
  23. opencos/eda.py +358 -155
  24. opencos/eda_base.py +187 -60
  25. opencos/eda_config.py +70 -35
  26. opencos/eda_config_defaults.yml +310 -186
  27. opencos/eda_config_reduced.yml +19 -39
  28. opencos/eda_tool_helper.py +190 -21
  29. opencos/files.py +26 -1
  30. opencos/tools/cocotb.py +11 -23
  31. opencos/tools/invio.py +2 -2
  32. opencos/tools/invio_yosys.py +2 -1
  33. opencos/tools/iverilog.py +3 -3
  34. opencos/tools/modelsim_ase.py +1 -1
  35. opencos/tools/quartus.py +172 -137
  36. opencos/tools/questa_common.py +50 -9
  37. opencos/tools/riviera.py +5 -4
  38. opencos/tools/slang.py +14 -10
  39. opencos/tools/slang_yosys.py +1 -0
  40. opencos/tools/surelog.py +7 -6
  41. opencos/tools/verilator.py +9 -7
  42. opencos/tools/vivado.py +315 -180
  43. opencos/tools/yosys.py +5 -5
  44. opencos/util.py +6 -3
  45. opencos/utils/dict_helpers.py +31 -0
  46. opencos/utils/markup_helpers.py +2 -2
  47. opencos/utils/str_helpers.py +38 -0
  48. opencos/utils/subprocess_helpers.py +3 -3
  49. opencos/utils/vscode_helper.py +2 -2
  50. opencos/utils/vsim_helper.py +16 -5
  51. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/METADATA +1 -1
  52. opencos_eda-0.3.12.dist-info/RECORD +93 -0
  53. opencos/eda_config_max_verilator_waivers.yml +0 -39
  54. opencos_eda-0.3.10.dist-info/RECORD +0 -81
  55. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/WHEEL +0 -0
  56. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/entry_points.txt +0 -0
  57. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/licenses/LICENSE +0 -0
  58. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/licenses/LICENSE.spdx +0 -0
  59. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/top_level.txt +0 -0
@@ -13,11 +13,70 @@ uses no tools and will print a help text regarding DEPS markup files to stdout.
13
13
  # pylint: disable=line-too-long
14
14
 
15
15
  import os
16
+ import re
16
17
 
17
18
  from opencos.eda_base import Command
18
19
  from opencos import util
19
20
  from opencos.util import Colors
20
21
 
22
+ def get_deps_md_file() -> str:
23
+ '''Tries to get docs/DEPS.md from our pypackage dist'''
24
+ opencos_dir, _ = os.path.split(util.__file__)
25
+
26
+ # Try to get it from site-packages dir, which should have docs/ alongside
27
+ # commands/ and tools/
28
+ filename = os.path.join(opencos_dir, 'docs', 'DEPS.md')
29
+ if os.path.isfile(filename):
30
+ return filename
31
+
32
+ # If you're running directly from the git checkout dir, you won't be getting
33
+ # this dist, so it's not in opencos/docs, it will simply be in ./docs
34
+ filename = os.path.join(opencos_dir, '..', 'docs', 'DEPS.md')
35
+ if os.path.isfile(filename):
36
+ return filename
37
+ return ''
38
+
39
+ def get_deps_md_contents() -> str:
40
+ '''Tries to get the docs/DEPS.md file and returns the str contents
41
+
42
+ This also performs some limited colorization of markdown and YAML
43
+ (assuming util was not disabled with --no-color)
44
+ '''
45
+ filename = get_deps_md_file()
46
+ if not filename:
47
+ return ''
48
+
49
+ def make_byellow(match):
50
+ '''Used by re.sub to wrap the match with bold yellow and return to normal yellow'''
51
+ return f'{Colors.byellow}{match.group(0)}{Colors.normal}{Colors.yellow}'
52
+
53
+ lines = []
54
+ with open(filename, encoding='utf-8') as f:
55
+ for line in f.readlines():
56
+
57
+ if line.startswith('# '):
58
+ # colors for markdown headings
59
+ line = f'{Colors.bgreen}{line}{Colors.normal}{Colors.yellow}'
60
+ elif line.startswith('## '):
61
+ # colors for markdown headings
62
+ line = f'{Colors.bcyan}{line}{Colors.normal}{Colors.yellow}'
63
+
64
+ elif '#' in line:
65
+ # colors for comments
66
+ line = line.replace('#', f'{Colors.normal}{Colors.cyan}#') + Colors.yellow
67
+
68
+ # colors for starting a line with:
69
+ # key: value
70
+ # - key : value
71
+ # try to make "key:" or "- key:" as bold:
72
+ line = re.sub(
73
+ r'^( *\-? ?[^ ]+):', make_byellow, line
74
+ )
75
+
76
+ lines.append(line)
77
+
78
+ return ''.join(lines)
79
+
21
80
 
22
81
  BASIC_DEPS_HELP = f'''
23
82
  {Colors.yellow}
@@ -128,119 +187,8 @@ FULL_DEPS_HELP = f'''
128
187
 
129
188
  {Colors.green}--------------------------------------------------------------------{Colors.yellow}
130
189
 
131
- Full {Colors.byellow}DEPS.yml{Colors.normal}{Colors.yellow} schema:
132
-
133
- ```
134
- DEFAULTS: # <table> defaults applied to ALL targets in this file, local targets ** override ** the defaults.
135
-
136
- METADATA: # <table> unstructured data, any UPPERCASE first level key is not considered a target.
137
-
138
- target-spec:
139
-
140
- args: # <array or | separated str>
141
- - --waves
142
- - --sim_plusargs="+info=500"
143
-
144
- defines: # <table>
145
- SOME_DEFINE: value
146
- SOME_DEFINE_NO_VALUE: # we just leave this blank, or use nil (yaml's None)
147
-
148
- plusargs: # <table>
149
- variable0: value
150
- variable1: # blank for no value, or use nil (yaml's None)
151
-
152
- parameters: # <table>
153
- SomeParameter: value
154
- SOME_OTHER_PARAMETER: value
155
-
156
- incdirs: # <array>
157
- - some/relative/path
158
-
159
- top: # <string>
160
-
161
- deps: # <array or | space separated string>
162
- - some_relative_target # <string> aka, a target
163
- - some_file.sv # <string> aka, a file
164
- - sv@some_file.txt # <string> aka, ext@file where we'd like a file not ending in .sv to be
165
- # treated as a .sv file for tools.
166
- # Supported for sv@, v@, vhdl@, cpp@, sdc@, f@, py@, makefile@
167
- - commands: # <table> with key 'commands' for a <array>: support for built-in commands
168
- # Note this cannot be confused for other targets or files.
169
- - shell: # <string>
170
- var-subst-args: # <bool> default false. If true, substitute vars in commands, such as {{fpga}}
171
- # substituted from eda arg --fpga=SomeFpga, such that {{fpga}} becomes SomeFpga
172
- var-subst-os-env: #<bool> default false. If true, substitute vars in commands using os.environ vars,
173
- # such as {{FPGA}} could get substituted by env value for $FPGA
174
- tee: # <string> optional filename, otherwise shell commands write to {{target-spec}}__shell_0.log
175
- run-from-work-dir: #<bool> default true. If false, runs from the directory of this DEPS file.
176
- filepath-subst-target-dir: #<bool> default true. If false, disables shell file path
177
- substituion on this target's directory (this DEPS file dir).
178
- dirpath-subst-target-dir: #<bool> default false. If true, enables shell directory path
179
- substituion on this target's directory (this DEPS file dir).
180
- run-after-tool: # <bool> default false. Set to true to run after any EDA tools, or
181
- any command handlers have completed.
182
- - shell: echo "Hello World!"
183
- - work-dir-add-sources: # <array or | space separated string>, this is how to add generated files
184
- # to compile order list.
185
- - peakrdl: # <string> ## peakrdl command to generate CSRs
186
-
187
- reqs: # <array or | space separated string>
188
- - some_file.mem # <string> aka, a non-source file required for this target.
189
- # This file is checked for existence prior to invoking the tool involved, for example,
190
- # in a simulation this would be done prior to a compile step.
191
-
192
- multi:
193
- ignore-this-target: # <array of tables> eda commands to be ignored in `eda multi <command>` for this target only
194
- # this is checked in the matching multi targets list, and is not inherited through dependencies.
195
- - commands: synth # space separated strings
196
- tools: vivado # space separated strings
197
-
198
- - commands: sim # omit tools, ignores 'sim' commands for all tools, for this target only, when this target
199
- # is in the target list called by `eda multi`.
200
-
201
- - tools: vivado # omit commands, ignores all commands if tool is vivado, for this target only, when this target
202
- # is in the target list called by `eda multi`.
203
-
204
- args: # <array> additional args added to all multi commands of this target.
205
- # Note that all args are POSIX with dashes, --sim-plusargs=value, etc.
206
-
207
- <eda-command>: # key is one of sim, flist, build, synth, etc.
208
- # can be used instead of 'tags' to support different args or deps.
209
- disable-tools: # Note: not implemented yet.
210
- only-tools: # Note: not implemented yet.
211
- args: # <array or | space separated string>
212
- deps: # <array or | space separated string> # Note: not implemented yet
213
- defines: ## <table>
214
- plusargs: ## <table>
215
- parameters: ## <table>
216
- incdirs: ## <array>
217
-
218
- tags: # <table> this is the currently support tags features in a target.
219
- <tag-name>: # <string> key for table, can be anything, name is not used.
220
- with-tools: <array or | space separated string>
221
- # If using one of these tools, apply these values.
222
- # entries can be in the form: vivado, or vivado:2024.1
223
- with-commands: <array or | space separated string>
224
- # apply if this was the `eda` command, such as: sim
225
- with-args: # <table> (optional) arg key/value pairs to match for this tag.
226
- # this would be an alternative to running eda with --tags=value
227
- # The existence of an argument with correct value would enable a tag.
228
- # And example would be:
229
- # with-args:
230
- # waves: true
231
- args: <array or | space separated string> # args to be applied if this target is used, with a matching
232
- # tool in 'with-tools'.
233
- deps: <array or | space separated string, applied with tag>
234
- defines: <table, applied with tag>
235
- plusargs: <table, applied with tag>
236
- parameters: <table, applied with tag>
237
- incdirs: <array, applied with tag>
238
- replace-config-tools: <table> # spec matching eda_config_defaults.yml::tools.<tool> (replace merge strategy)
239
- additive-config-tools: <table> # spec matching eda_config_defaults.yml::tools.<tool> (additive merge strategy)
240
-
241
-
242
- ```
243
- '''
190
+ ''' + get_deps_md_contents()
191
+
244
192
 
245
193
 
246
194
  class CommandDepsHelp:
@@ -275,4 +223,6 @@ class CommandDepsHelp:
275
223
  '''
276
224
  Command.help(self, tokens=tokens, no_targets=True)
277
225
  print()
226
+ print(BASIC_DEPS_HELP)
227
+ print()
278
228
  print(FULL_DEPS_HELP)
@@ -58,9 +58,14 @@ class CommandExport(CommandDesign):
58
58
 
59
59
  def do_it(self) -> None:
60
60
 
61
- # decide output dir name
61
+ # decide output dir name, note this does not follow the work-dir naming of
62
+ # eda.work/{target}.{command}
62
63
  if not self.args['output']:
63
- self.args['output'] = os.path.join('.', 'eda.export', self.args['top'] + '.export')
64
+ if self.target:
65
+ name = f'{self.target}.export'
66
+ else:
67
+ name = self.args.get('top', '') + '.export'
68
+ self.args['output'] = os.path.join('.', 'eda.export', name)
64
69
  out_dir = self.args['output']
65
70
 
66
71
  if not self.target:
opencos/commands/multi.py CHANGED
@@ -6,14 +6,14 @@ These are not intended to be overriden by child classes. They do not inherit Too
6
6
  import argparse
7
7
  import glob
8
8
  import os
9
- import shutil
10
9
  from pathlib import Path
11
10
 
12
11
  from opencos import util, eda_base, eda_config, export_helper, \
13
12
  eda_tool_helper
14
- from opencos.eda_base import CommandParallel, get_eda_exec
15
13
  from opencos.deps.deps_file import get_deps_markup_file, deps_markup_safe_load, \
16
14
  deps_data_get_all_targets, deps_list_target_sanitize
15
+ from opencos.eda_base import CommandParallel, get_eda_exec
16
+ from opencos.files import safe_shutil_which
17
17
  from opencos.utils.str_helpers import fnmatch_or_re, dep_str2list
18
18
 
19
19
  class CommandMulti(CommandParallel):
@@ -281,7 +281,7 @@ class CommandMulti(CommandParallel):
281
281
  if parsed.parallel < 1 or parsed.parallel > 256:
282
282
  self.error("Arg 'parallel' must be between 1 and 256")
283
283
 
284
- command = self.get_command_from_unparsed_args(tokens=unparsed)
284
+ command = self.get_sub_command_from_config()
285
285
 
286
286
  # Need to know the tool for this command, either it was set correctly via --tool and/or
287
287
  # the command (class) will tell us.
@@ -425,7 +425,7 @@ class CommandMulti(CommandParallel):
425
425
  '''
426
426
  eda_path = get_eda_exec('multi')
427
427
  command = self.single_command
428
- timeout = shutil.which('timeout')
428
+ timeout = safe_shutil_which('timeout')
429
429
 
430
430
  # Built-in support for running > 1 tool.
431
431
  all_multi_tools = self.multi_which_tools(command)
opencos/commands/sim.py CHANGED
@@ -370,7 +370,8 @@ class CommandSim(CommandDesign): # pylint: disable=too-many-public-methods
370
370
  def check_logs_for_errors( # pylint: disable=dangerous-default-value,too-many-locals,too-many-branches
371
371
  self,
372
372
  sim_retcode: int = 0,
373
- filename: str = '', file_contents_str: str = '',
373
+ filename: str = '',
374
+ file_contents_str: str = '',
374
375
  bad_strings: list = [], must_strings: list = [],
375
376
  warning_strings: list = [],
376
377
  use_bad_strings: bool = True, use_must_strings: bool = True
@@ -404,32 +405,30 @@ class CommandSim(CommandDesign): # pylint: disable=too-many-public-methods
404
405
  lines = []
405
406
 
406
407
  log_fpath = ''
407
- if os.path.exists(filename):
408
+ if os.path.isfile(filename):
408
409
  log_fpath = filename
409
410
 
410
411
  if file_contents_str:
411
412
  lines = file_contents_str.split('\n')
412
413
  log_fname = log_fpath + '(STDOUT)'
413
414
  util.debug(f'Checking log for errors: {log_fpath=} but checking from STDOUT string...')
414
- elif filename:
415
+ elif log_fpath:
415
416
  log_fname = log_fpath
416
- util.debug(f'Checking log for errors: {log_fpath=} opening file...')
417
- if not os.path.exists(log_fname):
418
- self.error(f'sim.check_logs_for_errors: log {log_fpath} does not exist, cannot',
419
- 'check it for errors or passing strings')
420
- return
417
+ util.info(f'Checking log filename: {log_fpath}')
421
418
  with open(log_fpath, 'r', encoding='utf-8') as f:
422
419
  lines = f.read().splitlines()
423
420
  else:
424
- self.error(f'sim.check_logs_for_errors: {log_fpath=} does not exist, and no',
425
- 'file_contents_str exists to check')
421
+ log_fname = filename
422
+ self.error(f'sim.check_logs_for_errors: {log_fname=} is not a file or does not exist,',
423
+ 'and no file_contents_str exists to check')
426
424
 
427
425
  self.update_tool_warn_err_counts_from_log_lines(
428
426
  log_lines=lines, bad_strings=_bad_strings, warning_strings=_warning_strings
429
427
  )
430
428
 
431
- if isinstance(self, Tool):
432
- self.report_tool_warn_error_counts()
429
+ func = getattr(self, 'report_tool_warn_error_counts', None)
430
+ if func and isinstance(self, Tool) and callable(func):
431
+ self.report_tool_warn_error_counts() # pylint: disable=no-member
433
432
 
434
433
  if sim_retcode > 0:
435
434
  # We have to update artifacts first, have the caller set the error.
@@ -446,14 +445,14 @@ class CommandSim(CommandDesign): # pylint: disable=too-many-public-methods
446
445
  hit_must_string_dict[k] = True
447
446
  if any(bad_str in line for bad_str in _bad_strings):
448
447
  self.error(
449
- f"log {log_fname}:{lineno} contains one of {_bad_strings=}",
448
+ f"log {log_fname}:{lineno} contains one of: {_bad_strings}",
450
449
  error_code=status_constants.EDA_SIM_LOG_HAS_BAD_STRING
451
450
  )
452
451
 
453
452
  if any(x is None for x in hit_must_string_dict.values()):
454
453
  self.error(
455
- f"Didn't get all passing patterns in log {log_fname}: {_must_strings=}",
456
- f" {hit_must_string_dict=}",
454
+ f"Didn't get all passing patterns in log {log_fname}: {_must_strings},",
455
+ f" {hit_must_string_dict}",
457
456
  error_code=status_constants.EDA_SIM_LOG_MISSING_MUST_STRING
458
457
  )
459
458
 
opencos/commands/sweep.py CHANGED
@@ -72,7 +72,7 @@ class CommandSweep(CommandDesign, CommandParallel):
72
72
 
73
73
  self.check_args()
74
74
 
75
- self.single_command = self.get_command_from_unparsed_args(tokens=unparsed)
75
+ self.single_command = self.get_sub_command_from_config()
76
76
 
77
77
  self._append_sweep_args(arg_tokens=arg_tokens)
78
78
 
opencos/commands/synth.py CHANGED
@@ -67,9 +67,8 @@ class CommandSynth(CommandDesign):
67
67
  if self.stop_process_tokens_before_do_it():
68
68
  return unparsed
69
69
 
70
- # add defines for this job type
71
70
  if self.args['top']:
72
- # create our work dir
71
+ # create our work dir (from self.args['top'])
73
72
  self.create_work_dir()
74
73
  self.run_dep_commands()
75
74
  self.do_it()
@@ -4,20 +4,73 @@ Intended to be overriden by Tool based classes (such as CommandUploadVivado, etc
4
4
  '''
5
5
 
6
6
  import os
7
+ import re
8
+ from datetime import datetime
9
+ from pathlib import Path
7
10
 
8
11
  from opencos.eda_base import Command, Tool
12
+ from opencos.util import Colors, debug, info, warning, safe_emoji, import_class_from_string
13
+
9
14
 
10
15
  class CommandUpload(Command):
11
- '''Base class command handler for: eda upload ...'''
16
+ '''Base class command handler for: eda upload ...
12
17
 
13
- CHECK_REQUIRES = [Tool]
18
+ If a --tool arg is not specified, this is the default handler for 'eda upload'
19
+ and will attempt to choose a derived class based on the bit files found
20
+ '''
14
21
 
15
22
  command_name = 'upload'
16
23
 
24
+ # SUPPORTED_TOOLS is used
25
+ SUPPORTED_TOOLS = {
26
+ 'vivado': ['.bit'],
27
+ 'quartus': ['.sof'],
28
+ }
29
+ BIT_EXT_TO_TOOL = {}
30
+
31
+ # Child classes can set SUPPORTED_BIT_EXT = ['.bit', ..] because they
32
+ # should only represent one tool
33
+ SUPPORTED_BIT_EXT = [item for value in SUPPORTED_TOOLS.values() for item in value]
34
+
35
+
17
36
  def __init__(self, config: dict):
18
37
  Command.__init__(self, config=config)
19
38
  self.unparsed_args = []
20
39
 
40
+ self.args.update({
41
+ 'bitfile': "",
42
+ 'list-bitfiles': False,
43
+ })
44
+
45
+ self.str_ext = '/'.join(self.SUPPORTED_BIT_EXT).replace('.', '').upper()
46
+
47
+ help_upload_tools = '|'.join(self.config.get('auto_tools_found', []))
48
+ if not help_upload_tools:
49
+ help_upload_tools = 'TOOL'
50
+
51
+ self.args_help.update({
52
+ 'bitfile': (
53
+ f'Tool specific {self.str_ext} files to upload (auto-detected if not specified)'
54
+ ' If you would like see full help for a given tool, use:'
55
+ f' {Colors.yellow}eda upload --help'
56
+ f' {Colors.byellow}--tool={help_upload_tools}{Colors.green}'
57
+ ),
58
+ 'list-bitfiles': (
59
+ f'List available {self.str_ext} files.'
60
+ ' If you would like see full help for a given tool, use:'
61
+ f' {Colors.yellow}eda upload --help'
62
+ f' {Colors.byellow}--tool={help_upload_tools}{Colors.green}'
63
+ )
64
+ })
65
+
66
+ self.bitfiles = []
67
+
68
+ if not getattr(self, '_TOOL', ''):
69
+ for tool, bit_exts in self.SUPPORTED_TOOLS.items():
70
+ for ext in bit_exts:
71
+ self.BIT_EXT_TO_TOOL[ext] = tool
72
+
73
+
21
74
  def process_tokens(
22
75
  self, tokens: list, process_all: bool = True, pwd: str = os.getcwd()
23
76
  ) -> list:
@@ -25,9 +78,144 @@ class CommandUpload(Command):
25
78
  self.unparsed_args = Command.process_tokens(
26
79
  self, tokens=tokens, process_all=False, pwd=pwd
27
80
  )
81
+
28
82
  if self.stop_process_tokens_before_do_it():
29
83
  return []
30
84
 
31
- self.create_work_dir()
32
- self.do_it()
85
+ self.bitfiles = self.get_list_bitfiles(display=True)
86
+
87
+ # If someone called --list-bitfiles, stop now.
88
+ if self.args['list-bitfiles']:
89
+ if not self.bitfiles:
90
+ self.error('No bitfiles found')
91
+ return []
92
+
93
+ sco = self._get_child_handling_class()
94
+
95
+ if sco is None or not isinstance(sco, Tool):
96
+ self.error('Could not find a suitable tool to process bitfiles')
97
+ return []
98
+
99
+ sco.unparsed_args = Command.process_tokens(
100
+ sco, tokens=tokens, process_all=False, pwd=pwd
101
+ )
102
+ sco.bitfiles = self.bitfiles
103
+ sco.create_work_dir()
104
+ sco.do_it()
33
105
  return []
106
+
107
+ def get_targets_or_files_from_unparsed_args(self) -> (list, list):
108
+ '''Returns (list of targets, list of files) from unparsed args or --bitfile'''
109
+
110
+ targets = []
111
+ files = []
112
+ for f in self.unparsed_args + [self.args['bitfile']]:
113
+ if not f:
114
+ continue
115
+ if os.path.isfile(f):
116
+ files.append(f)
117
+ elif not f.startswith('-'):
118
+ # avoid a arg
119
+ targets.append(f)
120
+ return targets, files
121
+
122
+
123
+ def get_list_bitfiles(self, display: bool = True) -> list:
124
+ '''Returns a list of bit files (ending with self.SUPPORTED_BIT_EXT)'''
125
+
126
+ bitfiles: list[Path] = []
127
+
128
+ targets, files = self.get_targets_or_files_from_unparsed_args()
129
+ targets.extend(files)
130
+
131
+ debug(f"Looking for bitfiles in {os.path.abspath('.')=}")
132
+ for root, _, files in os.walk("."):
133
+ for f in files:
134
+ if any(f.endswith(x) for x in self.SUPPORTED_BIT_EXT):
135
+ fullpath = os.path.abspath(Path(root) / f)
136
+ if os.path.isfile(fullpath) and fullpath not in bitfiles:
137
+ bitfiles.append(fullpath)
138
+
139
+ matched: list[Path] = []
140
+ for cand in bitfiles:
141
+ debug(f"Looking for {cand=} in {targets=}")
142
+ passing = all(re.search(t, str(cand)) for t in targets)
143
+ if passing:
144
+ matched.append(cand)
145
+ mod_time_string = datetime.fromtimestamp(
146
+ os.path.getmtime(cand)).strftime('%Y-%m-%d %H:%M:%S')
147
+ tool_guess = getattr(self, '_TOOL', '')
148
+ if not tool_guess:
149
+ ext = os.path.splitext(cand)[1]
150
+ tool_guess = self.BIT_EXT_TO_TOOL.get(ext, '')
151
+ if tool_guess:
152
+ tool_guess = f'({tool_guess})'
153
+ if display:
154
+ info(
155
+ f"{safe_emoji('⏩ ')}Found matching bitfile {tool_guess}:",
156
+ f"{Colors.cyan}{mod_time_string}{Colors.normal} :",
157
+ f"{Colors.byellow}{cand}"
158
+ )
159
+
160
+ if display and not matched:
161
+ if self.args['list-bitfiles']:
162
+ warning(f'{safe_emoji("❕ ")}--list-bitfiles: no {self.str_ext} found that matched',
163
+ f'{targets}')
164
+ else:
165
+ warning(f'{safe_emoji("❕ ")} Searched for bitfiles with {self.str_ext}: none found',
166
+ f'that matched {targets}')
167
+
168
+ return matched
169
+
170
+
171
+
172
+
173
+ def _get_child_handling_class(self) -> object:
174
+ '''Returns a class handle of a child to process this, which should be a Tool class
175
+
176
+ if no appropriate child is found, returns self.
177
+ '''
178
+
179
+ if isinstance(self, Tool):
180
+ # We're already a tool handling class.
181
+ return self
182
+
183
+ tools_found = set()
184
+ for bitfile in self.bitfiles:
185
+ ext = os.path.splitext(bitfile)[1]
186
+ tool_guess = self.BIT_EXT_TO_TOOL.get(ext, '')
187
+ if tool_guess:
188
+ tools_found.add(tool_guess)
189
+ else:
190
+ warning(f'For bitfile {bitfile} no tool found for it')
191
+ return self
192
+
193
+ if not tools_found:
194
+ # Probably not an error?
195
+ warning(f'No tools found to process bitfiles: {self.bitfiles}')
196
+ return self
197
+
198
+ if len(tools_found) > 1:
199
+ warning(f'More than one tool found ({tools_found}) to to process bitfiles:',
200
+ f'{self.bitfiles}')
201
+ return self
202
+
203
+
204
+ tool = tools_found.pop() # only item in set
205
+ # Do we have a handler for this in our config?
206
+ if tool in self.config.get('tools_loaded', []):
207
+ tool_cfg = self.config.get('tools', {}).get(tool, {})
208
+ if tool_cfg:
209
+ cls_str = tool_cfg.get('handlers', {}).get(self.command_name, None)
210
+ if cls_str:
211
+ cls = import_class_from_string(cls_str)
212
+ if issubclass(cls, Command):
213
+ info(f'For found bitfiles, can use tool={tool} and handler {cls}')
214
+ sco = cls(config=self.config)
215
+ return sco
216
+
217
+
218
+ warning(f'No handler found for tool={tool} to process bitfiles: {self.bitfiles}')
219
+ debug(f'config -- tools_loaded: {self.config["tools_loaded"]}')
220
+ debug(f'config -- tools for tool: {self.config["tools"].get(tool, "")}')
221
+ return self
opencos/commands/waves.py CHANGED
@@ -15,10 +15,12 @@ handler).
15
15
  # pylint: disable=R0801
16
16
 
17
17
  import os
18
- import shutil
18
+ import subprocess
19
19
 
20
20
  from opencos import util
21
21
  from opencos.eda_base import CommandDesign
22
+ from opencos.files import safe_shutil_which
23
+ from opencos.utils import vscode_helper
22
24
 
23
25
 
24
26
  class CommandWaves(CommandDesign):
@@ -51,6 +53,48 @@ class CommandWaves(CommandDesign):
51
53
  + ' to stdout',
52
54
  })
53
55
 
56
+ def get_versions_of_tool(self, tool: str) -> str:
57
+ '''Similar to Tool.get_versions(), returns the version of 'tool' for tools like:
58
+
59
+ - vaporview
60
+ - gtkwave
61
+
62
+ This is called by eda_tool_helper.get_handler_tool_version(tool, cmd, config)
63
+ '''
64
+
65
+ entry = self.config.get('tools', {}).get(tool, {})
66
+
67
+ if entry and 'requires_vscode_extension' in entry:
68
+ # vaporview, surfer
69
+ vscode_ext_name = entry.get('requires_vscode_extension', [''])[0]
70
+ vscode_helper.init()
71
+ ver = vscode_helper.EXTENSIONS.get(vscode_ext_name)
72
+ return ver
73
+
74
+ if entry and tool == 'gtkwave':
75
+ # gtkwave --version is fast.
76
+ proc = None
77
+ try:
78
+ proc = subprocess.run(
79
+ [safe_shutil_which('gtkwave'), '--version'],
80
+ capture_output=True, check=False
81
+ )
82
+ except Exception as e:
83
+ util.debug(f'gtkwave --version: exception {e}')
84
+
85
+ if not proc or not proc.stdout:
86
+ return ''
87
+
88
+ for line in proc.stdout.decode('utf-8', errors='replace').split('\n'):
89
+ if line.lower().startswith('gtkwave analyzer v'):
90
+ parts = line.split(' ')
91
+ return parts[2][1:] # trim the leading 'v' in 'v1.2.3'
92
+ return ''
93
+
94
+ return ''
95
+
96
+
97
+
54
98
 
55
99
  def get_wave_files_in_dirs(self, wave_dirs: list, quiet: bool = False) -> list:
56
100
  '''Returns list of all wave files give wave_dirs (list)'''
@@ -120,10 +164,10 @@ class CommandWaves(CommandDesign):
120
164
 
121
165
  # TODO(drew): this feels a little customized per-tool, perhaps there's a better
122
166
  # way to abstract this configuration for adding other waveform viewers.
123
- # For example for each command we also have to check shutil.which, because normal Tool
167
+ # For example for each command we also have to check safe_shutil_which, because normal Tool
124
168
  # classs should work even w/out PATH, but these don't use Tool classes.
125
169
  if wave_file.endswith('.wdb'):
126
- if 'vivado' in self.config['tools_loaded'] and shutil.which('vivado'):
170
+ if 'vivado' in self.config['tools_loaded'] and safe_shutil_which('vivado'):
127
171
  tcl_name = wave_file + '.waves.tcl'
128
172
  with open( tcl_name, 'w', encoding='utf-8') as fo :
129
173
  print( 'current_fileset', file=fo)
@@ -141,20 +185,20 @@ class CommandWaves(CommandDesign):
141
185
  f"{self.VSIM_TOOLS} in PATH")
142
186
  elif wave_file.endswith('.fst'):
143
187
  if ('vaporview' in self.config['tools_loaded'] or \
144
- 'surfer' in self.config['tools_loaded']) and shutil.which('code'):
188
+ 'surfer' in self.config['tools_loaded']) and safe_shutil_which('code'):
145
189
  command_list = ['code', '-n', '.', wave_file]
146
190
  self.exec(os.path.dirname(wave_file), command_list)
147
- elif 'gtkwave' in self.config['tools_loaded'] and shutil.which('gtkwave'):
191
+ elif 'gtkwave' in self.config['tools_loaded'] and safe_shutil_which('gtkwave'):
148
192
  command_list = ['gtkwave', wave_file]
149
193
  self.exec(os.path.dirname(wave_file), command_list)
150
194
  else:
151
195
  self.error(f"Don't know how to open {wave_file} without GtkWave in PATH")
152
196
  elif wave_file.endswith('.vcd'):
153
197
  if ('vaporview' in self.config['tools_loaded'] or \
154
- 'surfer' in self.config['tools_loaded']) and shutil.which('code'):
198
+ 'surfer' in self.config['tools_loaded']) and safe_shutil_which('code'):
155
199
  command_list = ['code', '-n', '.', wave_file]
156
200
  self.exec(os.path.dirname(wave_file), command_list)
157
- elif 'gtkwave' in self.config['tools_loaded'] and shutil.which('gtkwave'):
201
+ elif 'gtkwave' in self.config['tools_loaded'] and safe_shutil_which('gtkwave'):
158
202
  command_list = ['gtkwave', wave_file]
159
203
  self.exec(os.path.dirname(wave_file), command_list)
160
204
  elif self._vsim_available(from_tools=self.VSIM_VCD_TOOLS):
@@ -172,7 +216,7 @@ class CommandWaves(CommandDesign):
172
216
  self, from_tools: list = VSIM_TOOLS
173
217
  ) -> bool:
174
218
  '''Returns True if 'vsim' is available (Questa or Modelsim)'''
175
- return bool(shutil.which('vsim')) and \
219
+ return bool(safe_shutil_which('vsim')) and \
176
220
  any(x in self.config['tools_loaded'] for x in from_tools)
177
221
 
178
222