opencos-eda 0.2.28__py3-none-any.whl → 0.2.32__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 (35) hide show
  1. opencos/commands/export.py +2 -1
  2. opencos/commands/flist.py +49 -12
  3. opencos/commands/multi.py +101 -130
  4. opencos/commands/synth.py +0 -1
  5. opencos/commands/upload.py +7 -7
  6. opencos/commands/waves.py +50 -19
  7. opencos/deps_helpers.py +93 -3
  8. opencos/deps_schema.py +28 -18
  9. opencos/eda.py +6 -1
  10. opencos/eda_base.py +29 -10
  11. opencos/eda_config.py +1 -0
  12. opencos/eda_config_defaults.yml +4 -1
  13. opencos/eda_extract_deps_keys.py +27 -10
  14. opencos/files.py +6 -8
  15. opencos/names.py +1 -0
  16. opencos/oc_cli.py +143 -1
  17. opencos/peakrdl_cleanup.py +0 -1
  18. opencos/tests/helpers.py +38 -10
  19. opencos/tests/test_deps_helpers.py +46 -2
  20. opencos/tests/test_eda.py +65 -41
  21. opencos/tests/test_tools.py +17 -9
  22. opencos/tools/iverilog.py +0 -2
  23. opencos/tools/modelsim_ase.py +11 -5
  24. opencos/tools/questa.py +14 -19
  25. opencos/tools/verilator.py +0 -2
  26. opencos/tools/vivado.py +253 -112
  27. opencos/tools/yosys.py +1 -6
  28. opencos/util.py +4 -0
  29. {opencos_eda-0.2.28.dist-info → opencos_eda-0.2.32.dist-info}/METADATA +1 -1
  30. {opencos_eda-0.2.28.dist-info → opencos_eda-0.2.32.dist-info}/RECORD +35 -35
  31. {opencos_eda-0.2.28.dist-info → opencos_eda-0.2.32.dist-info}/WHEEL +1 -1
  32. {opencos_eda-0.2.28.dist-info → opencos_eda-0.2.32.dist-info}/entry_points.txt +0 -0
  33. {opencos_eda-0.2.28.dist-info → opencos_eda-0.2.32.dist-info}/licenses/LICENSE +0 -0
  34. {opencos_eda-0.2.28.dist-info → opencos_eda-0.2.32.dist-info}/licenses/LICENSE.spdx +0 -0
  35. {opencos_eda-0.2.28.dist-info → opencos_eda-0.2.32.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  '''opencos.commands.export - Base class command handler for: eda export ...
2
2
 
3
- Intended to be overriden by Tool based classes (such as CommandExportVivado, etc)'''
3
+ Intended to be overriden by Tool based classes (such as CommandExportVivado, etc), although
4
+ `eda export` can be run without --tool.'''
4
5
 
5
6
  # Note - similar code waiver, tricky to eliminate it with inheritance when
6
7
  # calling reusable methods.
opencos/commands/flist.py CHANGED
@@ -34,9 +34,17 @@ class CommandFList(CommandDesign):
34
34
  'prefix-vhd' : "",
35
35
  'prefix-cpp' : "",
36
36
  'prefix-non-sources' : "", # as comments anyway.
37
+ # TODO(simon): make the define quoting work like the paths quoting
38
+ 'bracket-quote-define': False,
37
39
  'single-quote-define': False,
38
40
  'quote-define' : True,
39
- 'xilinx' : False, # convience arg for Vivado tools
41
+ 'equal-define' : True,
42
+ 'escape-define-value': False,
43
+ 'quote-define-value' : False,
44
+ 'bracket-quote-path' : False,
45
+ 'single-quote-path' : False,
46
+ 'double-quote-path' : False,
47
+ 'no-quote-path' : False,
40
48
  'build-script' : "", # we don't want this to error either
41
49
 
42
50
  'print-to-stdout': False, # do not save to file, print to stdout.
@@ -102,6 +110,20 @@ class CommandFList(CommandDesign):
102
110
  self.create_work_dir()
103
111
  self.run_dep_commands()
104
112
 
113
+ pq1 = ""
114
+ pq2 = "" # pq = path quote
115
+ if self.args['no-quote-path']:
116
+ pass # if we decide to make one of the below default, this will override
117
+ elif self.args['bracket-quote-path']:
118
+ pq1 = "{"
119
+ pq2 = "}"
120
+ elif self.args['single-quote-path']:
121
+ pq1 = "'"
122
+ pq2 = "'"
123
+ elif self.args['double-quote-path']:
124
+ pq1 = '"'
125
+ pq2 = '"'
126
+
105
127
  if self.args['print-to-stdout']:
106
128
  fo = None
107
129
  print()
@@ -119,7 +141,7 @@ class CommandFList(CommandDesign):
119
141
  for f in self.files_non_source:
120
142
  if self.args['emit-rel-path']:
121
143
  f = os.path.relpath(f)
122
- print('## ' + prefix + f, file=fo)
144
+ print('## ' + prefix + pq1 + f + pq2, file=fo)
123
145
 
124
146
  if self.args['emit-define']:
125
147
  prefix = util.strip_all_quotes(self.args['prefix-define'])
@@ -127,44 +149,59 @@ class CommandFList(CommandDesign):
127
149
  if value is None:
128
150
  newline = prefix + d
129
151
  else:
130
- if self.args['single-quote-define']:
131
- quote = '\''
152
+ if self.args['bracket-quote-define']:
153
+ qd1 = "{"
154
+ qd2 = "}"
155
+ elif self.args['single-quote-define']:
156
+ qd1 = "'"
157
+ qd2 = "'"
132
158
  elif self.args['quote-define']:
133
- quote = '"'
159
+ qd1 = '"'
160
+ qd2 = '"' # rename this one when things calm down
161
+ else:
162
+ qd1 = ''
163
+ qd2 = ''
164
+ if self.args['equal-define']:
165
+ ed1 = '='
134
166
  else:
135
- quote = ''
136
- newline = prefix + quote + f"{d}={value}" + quote
167
+ ed1 = ' '
168
+ if self.args['escape-define-value']:
169
+ value = value.replace('\\', '\\\\').replace('"', '\\"')
170
+ if self.args['quote-define-value']:
171
+ value =f'"{value}"'
172
+ newline = prefix + qd1 + f"{d}{ed1}{value}" + qd2
137
173
  print(newline, file=fo)
174
+
138
175
  if self.args['emit-incdir']:
139
176
  prefix = util.strip_all_quotes(self.args['prefix-incdir'])
140
177
  for i in self.incdirs:
141
178
  if self.args['emit-rel-path']:
142
179
  i = os.path.relpath(i)
143
- print(prefix + i, file=fo)
180
+ print(prefix + pq1 + i + pq2, file=fo)
144
181
  if self.args['emit-v']:
145
182
  prefix = util.strip_all_quotes(self.args['prefix-v'])
146
183
  for f in self.files_v:
147
184
  if self.args['emit-rel-path']:
148
185
  f = os.path.relpath(f)
149
- print(prefix + f, file=fo)
186
+ print(prefix + pq1 + f + pq2, file=fo)
150
187
  if self.args['emit-sv']:
151
188
  prefix = util.strip_all_quotes(self.args['prefix-sv'])
152
189
  for f in self.files_sv:
153
190
  if self.args['emit-rel-path']:
154
191
  f = os.path.relpath(f)
155
- print(prefix + f, file=fo)
192
+ print(prefix + pq1 + f + pq2, file=fo)
156
193
  if self.args['emit-vhd']:
157
194
  prefix = util.strip_all_quotes(self.args['prefix-vhd'])
158
195
  for f in self.files_vhd:
159
196
  if self.args['emit-rel-path']:
160
197
  f = os.path.relpath(f)
161
- print(prefix + f, file=fo)
198
+ print(prefix + pq1 + f + pq2, file=fo)
162
199
  if self.args['emit-cpp']:
163
200
  prefix = util.strip_all_quotes(self.args['prefix-cpp'])
164
201
  for f in self.files_cpp:
165
202
  if self.args['emit-rel-path']:
166
203
  f = os.path.relpath(f)
167
- print(prefix + f, file=fo)
204
+ print(prefix + pq1 + f + pq2, file=fo)
168
205
 
169
206
  if self.args['print-to-stdout']:
170
207
  print() # don't need to close fo (None)
opencos/commands/multi.py CHANGED
@@ -6,8 +6,8 @@ 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 re
10
9
  import shutil
10
+ from pathlib import Path
11
11
 
12
12
  from opencos import util, eda_base, deps_helpers, eda_config, export_helper, \
13
13
  eda_tool_helper
@@ -21,25 +21,43 @@ class CommandMulti(CommandParallel):
21
21
 
22
22
  def __init__(self, config: dict):
23
23
  CommandParallel.__init__(self, config=config)
24
- self.args.update({
24
+
25
+ self.multi_only_args = {
25
26
  'fake': False,
26
27
  'parallel': 1,
27
28
  'single-timeout': None,
28
29
  'fail-if-no-targets': False,
29
30
  'export-jsonl': False,
30
- })
31
+ 'print-targets': False,
32
+ }
33
+
34
+ self.args.update(self.multi_only_args)
31
35
  self.args_help.update({
32
36
  'single-timeout': ('shell timeout on a single operation in multi, not the entire'
33
37
  'multi command'),
34
38
  'fail-if-no-targets': 'fails the multi command if no targets were found',
35
39
  'export-jsonl': ('If set, generates export.jsonl if possible, spawns single commands'
36
40
  'with --export-json'),
41
+ 'print-targets': 'Do not run jobs, prints targets to stdout',
37
42
  })
38
43
  self.single_command = ''
39
44
  self.targets = [] # list of tuples (target:str, tool:str)
40
45
  self.resolve_target_command = ''
41
46
 
42
47
 
48
+ def path_hidden_or_work_dir(self, path: str) -> bool:
49
+ '''Returns True if any portion of path is hidden file/dir or has a work-dir
50
+
51
+ such as "eda.work" (self.args['eda-dir'])'''
52
+
53
+ path_obj = Path(os.path.abspath(path))
54
+ if self.args['eda-dir'] in path_obj.parts:
55
+ return True
56
+ if any(x.startswith('.') and len(x) > 1 for x in path_obj.parts):
57
+ return True
58
+ return False
59
+
60
+
43
61
  def resolve_target_get_command_level(self, level: int = -1) -> (str, int):
44
62
  '''increments level, returns tuple of (command str, level int)'''
45
63
  command = self.resolve_target_command
@@ -49,13 +67,55 @@ class CommandMulti(CommandParallel):
49
67
  level += 1
50
68
  return command, level
51
69
 
52
- @staticmethod
53
- def resolve_target_get_path_parts(target: str) -> (str, list):
54
- '''Returns (target, path part list).
55
70
 
56
- Strips outer " and splits on / (POSIX file path only)'''
57
- target = target.strip('"').strip("'")
58
- return target, target.split("/")
71
+ def resolve_path_and_target_patterns(
72
+ self, base_path: str, target: str, level: int = -1
73
+ ) -> dict:
74
+ '''Returns a dict of: key = matching path, value = set of matched targets.
75
+
76
+ Looks at globbed paths from base_path/target, and looks for DEPS markup targets
77
+ matching target_pattern (using fnmatch or re.match)
78
+ '''
79
+ def debug(*text):
80
+ util.debug(f'resolve_target() {level=} {base_path=}', *text)
81
+
82
+ # join base_path / target
83
+ # - if target = ./some_path/**/*test
84
+ # split them again so we can do all globbing on the path_pattern portion:
85
+ path_pattern, target_pattern = os.path.split(os.path.join(base_path, target))
86
+ debug(f'{path_pattern=}, {target_pattern=}')
87
+
88
+ if target_pattern == '...':
89
+ # replace bazel style target ... with '*' for fnmatch.
90
+ target_pattern = '*'
91
+
92
+ matching_targets_dict = {}
93
+
94
+ # resolve the path_pattern portion using glob.
95
+ # we'll have to check for DEPS markup files in path_pattern, to match the target_wildcard
96
+ # using fnmatch or re.
97
+ for path in glob.glob(path_pattern, recursive=True):
98
+
99
+ if self.path_hidden_or_work_dir(path):
100
+ continue
101
+
102
+ deps_markup_file = deps_helpers.get_deps_markup_file(path)
103
+ if deps_markup_file:
104
+ data = deps_helpers.deps_markup_safe_load(deps_markup_file)
105
+ deps_targets = deps_helpers.deps_data_get_all_targets(data)
106
+ rel_path = os.path.relpath(path)
107
+
108
+ debug(f'in {rel_path=} looking for {target_pattern=} in {deps_targets=}')
109
+
110
+ for t in deps_targets:
111
+ if deps_helpers.fnmatch_or_re(pattern=target_pattern, string=t):
112
+ if rel_path not in matching_targets_dict:
113
+ matching_targets_dict[rel_path] = set()
114
+ matching_targets_dict[rel_path].add(t)
115
+
116
+ debug(f'Found potential targets for {target_pattern=}: {matching_targets_dict=}')
117
+ return matching_targets_dict
118
+
59
119
 
60
120
  def resolve_target(self, base_path: str, target: str, level: int = -1) -> None:
61
121
  '''Returns None, recursively attempts to determine the validity of a base_path/target,
@@ -66,37 +126,36 @@ class CommandMulti(CommandParallel):
66
126
  '''
67
127
 
68
128
  def debug(*text):
69
- util.debug('resolve_target() {level=} {base_path=}', *text)
129
+ util.debug(f'resolve_target() {level=} {base_path=}', *text)
70
130
 
71
131
  command, level = self.resolve_target_get_command_level(level)
72
132
 
73
133
  debug(f"Enter/Start: target={target}, command={command}")
74
134
 
75
- target, target_path_parts = self.resolve_target_get_path_parts(target)
76
-
77
- if len(target_path_parts) == 1:
78
- debug(f"{target} is a single-part target, look for matches in here")
79
-
80
- target_pattern = "^" + target_path_parts.pop(0) + "$"
81
- target_pattern = target_pattern.replace("*", r"[^\/]*")
82
- debug(f"{target_pattern=}")
83
- self.resolve_target_single(base_path=base_path, target=target_pattern, level=level)
135
+ # Strip outer quote on target, in case it was passed this way from CLI:
136
+ for x in ['"', "'"]:
137
+ target = target.lstrip(x).rstrip(x)
84
138
 
85
- else:
139
+ matching_targets_dict = self.resolve_path_and_target_patterns(
140
+ base_path=base_path, target=target, level=level
141
+ )
86
142
 
87
- self.resolve_target_path_parts(base_path=base_path, target=target, level=level)
143
+ for path, targets in matching_targets_dict.items():
144
+ self.resolve_target_single_path(base_path=path, targets=targets, level=level)
88
145
 
89
146
 
90
- def resolve_target_single( # pylint: disable=too-many-locals
91
- self, base_path: str, target: str, level: int = -1
147
+ def resolve_target_single_path( # pylint: disable=too-many-locals
148
+ self, base_path: str, targets: list, level: int = -1
92
149
  ) -> None:
93
- '''Returns None, called by resolve_target(..) if we have a single target,
150
+ '''Returns None, called by resolve_target(..) if we have a single base_path,
94
151
 
95
- and need to resolve it via path and DEPS.[markup] file information.
152
+ and multiple targets (list), and need to resolve it via base_path and DEPS.[markup]
153
+ file information. There should be no remaining wildcard information in targets
154
+ (that was handled earlier with fnmatch and re.)
96
155
  '''
97
156
 
98
157
  def debug(*text):
99
- util.debug('resolve_target() {level=} {base_path=}', *text)
158
+ util.debug(f'resolve_target() {level=} {base_path=}', *text)
100
159
 
101
160
  command, level = self.resolve_target_get_command_level(level)
102
161
 
@@ -110,20 +169,15 @@ class CommandMulti(CommandParallel):
110
169
  if data is None:
111
170
  data = {}
112
171
 
172
+ deps_targets = deps_helpers.deps_data_get_all_targets(data)
113
173
  deps_file_defaults = data.get('DEFAULTS', {})
114
174
 
115
175
  # Loop through all the targets in DEPS.yml, skipping DEFAULTS
116
- for target_node, entry in data.items():
117
-
118
- # Skip upper-case targets, including 'DEFAULTS':
119
- if target_node == target_node.upper():
176
+ for target_node in targets:
177
+ if target_node not in deps_targets:
120
178
  continue
121
179
 
122
- m = re.match(target, target_node)
123
- if not m:
124
- # If the target_node in our deps_file doesn't
125
- # match the pattern, then skip.
126
- continue
180
+ entry = data[target_node]
127
181
 
128
182
  # Since we support a few schema flavors for a target (our
129
183
  # 'target_node' key in a DEPS.yml file) santize the entry
@@ -182,96 +236,9 @@ class CommandMulti(CommandParallel):
182
236
 
183
237
  for tool in all_multi_tools:
184
238
  if tool not in multi_ignore_skip_this_target_node:
185
- debug(f"Found dep {target_node=} {tool=} matching",
186
- f"{target=} {entry=}")
239
+ debug(f"Found dep {target_node=} {tool=} matching, {entry=}")
187
240
  self.targets.append( tuple([os.path.join(base_path, target_node), tool]) )
188
241
 
189
- def resolve_target_path_parts( # pylint: disable=too-many-branches
190
- self, base_path: str, target: str, level: int = -1
191
- ) -> None:
192
- '''Returns None, recursively attempts to determine the validity of a base_path/target,
193
-
194
- operates on a directory, will re-invoke resolve_target(...).
195
- '''
196
-
197
- _, level = self.resolve_target_get_command_level(level)
198
-
199
- def debug(*text):
200
- util.debug('resolve_target() {level=} {base_path=}', *text)
201
-
202
- target, target_path_parts = self.resolve_target_get_path_parts(target)
203
-
204
- # let's look at the first part of the multi-part target path, which should be a dir
205
- part = target_path_parts.pop(0)
206
- if part == ".":
207
- # just reprocess this directory (matches "./some/path" and retries as "some/path")
208
- debug(f"processing {part}, recursing here")
209
- self.resolve_target(
210
- base_path=base_path, target=os.path.sep.join(target_path_parts),
211
- level=level
212
- )
213
-
214
- elif part == "..":
215
- # reprocess from the directory above (../some/path --> change base_path, some/path)
216
- debug(f"processing {part}, recursing above at ../")
217
- new_base_path = os.path.abspath(os.path.join(base_path, part))
218
- self.resolve_target(
219
- base_path=new_base_path, target=os.path.sep.join(target_path_parts),
220
- level=level
221
- )
222
-
223
- elif part == "...":
224
- # support ... as bazel-style all subdirs.
225
- debug(f"processing {part}, recursing to check here")
226
- # first we check this dir: {"<base>",".../target"} should match "target" in <base>,
227
- # so we call {"<base>","target"}
228
- self.resolve_target(
229
- base_path=base_path, target=os.path.sep.join(target_path_parts),
230
- level=level
231
- )
232
- # now we find all dirs in <base> ...
233
- debug(f"processing {part}, looking through dirs...")
234
- for e in os.listdir(base_path):
235
- debug(f"{e=}, isdir={os.path.isdir(os.path.join(base_path,e))}")
236
- if e in ('eda.work', self.args['eda-dir']):
237
- debug(f"processing {part}, skipping work dir {e}")
238
- elif os.path.islink(os.path.join(base_path, e)):
239
- debug(f"processing {part}, skipping link dir {e}")
240
- elif os.path.isdir(os.path.join(base_path,e)):
241
- debug(f"processing {part}, recursing into {e}")
242
- self.resolve_target(
243
- base_path=os.path.join(base_path, e), target=target,
244
- level=level)
245
- elif part.startswith("."):
246
- debug(f"processing {part}, skipping hidden")
247
- elif part == self.args['eda-dir']:
248
- debug(f"processing {part}, skipping eda.dir")
249
- elif os.path.isdir(os.path.join(base_path, part)):
250
- # reprocess in a lower directory (matches "some/...",
251
- # enters "some/", and retries "...")
252
- debug(f"processing {part}, recursing down")
253
- self.resolve_target(
254
- base_path=os.path.join(base_path, part),
255
- target=os.path.sep.join(target_path_parts), level=level
256
- )
257
- elif part == "*":
258
- # descend into every directory, we only go in if there's a DEPS though
259
- debug(f"processing {part}, looking through dirs...")
260
- for e in os.listdir(base_path):
261
- debug(f"{e=}")
262
- if os.path.isdir(e):
263
- debug(f"looking for ={os.path.join(base_path, e, 'DEPS-markup-file')}")
264
- deps_markup_file = get_deps_markup_file(os.path.join(base_path, e))
265
- if self.config['deps_markup_supported'] and deps_markup_file:
266
- self.resolve_target(
267
- base_path=os.path.join(base_path, e),
268
- target=os.path.sep.join(target_path_parts),
269
- level=level)
270
- else:
271
- # This is not a warning/error, just means we didn't find what we were looking
272
- # for on expanding a path part with * and ... in the target.
273
- debug(f"processing {part} ... but not sure what to do with it?")
274
-
275
242
 
276
243
  def process_tokens( # pylint: disable=too-many-locals, too-many-branches, too-many-statements
277
244
  self, tokens: list, process_all: bool = True, pwd: str = os.getcwd()
@@ -294,9 +261,7 @@ class CommandMulti(CommandParallel):
294
261
  # only run it on a subset of our self.args:
295
262
  parsed, unparsed = self.run_argparser_on_list(
296
263
  tokens=tokens,
297
- parser_arg_list=[
298
- 'fake', 'parallel', 'single-timeout', 'fail-if-no-targets', 'export-jsonl'
299
- ],
264
+ parser_arg_list=list(self.multi_only_args.keys()),
300
265
  apply_parsed_args=True
301
266
  )
302
267
 
@@ -372,10 +337,16 @@ class CommandMulti(CommandParallel):
372
337
  mylist = get_pretty_targets_tuple_as_list(self.targets)
373
338
  util.info( ", ".join(mylist), start="")
374
339
 
375
- util.debug("Multi: converting list of targets into list of jobs")
376
- self.jobs = []
377
- self.append_jobs_from_targets(args=arg_tokens)
378
- self.run_jobs(command)
340
+ if self.args['print-targets']:
341
+ util.info('Multi print-targets (will not run jobs): -->')
342
+ for t in self.targets:
343
+ # t = tuple of (target:str, tool:str), we just want the target.
344
+ print(f' {t[0]}')
345
+ else:
346
+ util.debug("Multi: converting list of targets into list of jobs")
347
+ self.jobs = []
348
+ self.append_jobs_from_targets(args=arg_tokens)
349
+ self.run_jobs(command)
379
350
 
380
351
  # Because CommandMulti has a custom arg parsing, we do not have 'export' related
381
352
  # args in self.args (they are left as 'unparsed' for the glob'ed commands)
opencos/commands/synth.py CHANGED
@@ -102,7 +102,6 @@ class CommandSynth(CommandDesign):
102
102
  deps_file_args = []
103
103
  for a in self.get_command_line_args():
104
104
  if any(a.startswith(x) for x in [
105
- '--xilinx',
106
105
  '--optimize',
107
106
  '--synth',
108
107
  '--idelay',
@@ -5,9 +5,9 @@ Intended to be overriden by Tool based classes (such as CommandUploadVivado, etc
5
5
 
6
6
  import os
7
7
 
8
- from opencos.eda_base import CommandDesign, Tool
8
+ from opencos.eda_base import Command, Tool
9
9
 
10
- class CommandUpload(CommandDesign):
10
+ class CommandUpload(Command):
11
11
  '''Base class command handler for: eda upload ...'''
12
12
 
13
13
  CHECK_REQUIRES = [Tool]
@@ -15,16 +15,16 @@ class CommandUpload(CommandDesign):
15
15
  command_name = 'upload'
16
16
 
17
17
  def __init__(self, config: dict):
18
- CommandDesign.__init__(self, config=config)
18
+ Command.__init__(self, config=config)
19
+ self.unparsed_args = []
19
20
 
20
21
  def process_tokens(
21
22
  self, tokens: list, process_all: bool = True, pwd: str = os.getcwd()
22
23
  ) -> list:
23
24
 
24
- unparsed = CommandDesign.process_tokens(
25
- self, tokens=tokens, process_all=process_all, pwd=pwd
25
+ self.unparsed_args = Command.process_tokens(
26
+ self, tokens=tokens, process_all=False, pwd=pwd
26
27
  )
27
28
  self.create_work_dir()
28
- self.run_dep_commands()
29
29
  self.do_it()
30
- return unparsed
30
+ return []
opencos/commands/waves.py CHANGED
@@ -30,9 +30,24 @@ class CommandWaves(CommandDesign):
30
30
  '.wdb', '.vcd', '.wlf', '.fst'
31
31
  ]
32
32
 
33
+ VSIM_TOOLS = set([
34
+ 'questa',
35
+ 'modelsim_ase',
36
+ ])
37
+
38
+ VSIM_VCD_TOOLS = set([
39
+ 'questa',
40
+ ])
33
41
 
34
42
  def __init__(self, config: dict):
35
43
  CommandDesign.__init__(self, config=config)
44
+ self.args.update({
45
+ 'test-mode': False,
46
+ })
47
+ self.args_help.update({
48
+ 'test-mode': 'Do not run the command to open the located wave file, instead print' \
49
+ + ' to stdout',
50
+ })
36
51
 
37
52
 
38
53
  def get_wave_files_in_dirs(self, wave_dirs: list, quiet: bool = False) -> list:
@@ -62,6 +77,9 @@ class CommandWaves(CommandDesign):
62
77
  wave_dirs = []
63
78
  tokens = CommandDesign.process_tokens(self, tokens=tokens, process_all=False, pwd=pwd)
64
79
 
80
+ if self.args['test-mode']:
81
+ self.exec = self._test_mode_exec
82
+
65
83
  while tokens:
66
84
  if os.path.isfile(tokens[0]):
67
85
  if wave_file is not None:
@@ -87,8 +105,8 @@ class CommandWaves(CommandDesign):
87
105
  wave_dirs.append('.')
88
106
  all_files = self.get_wave_files_in_dirs(wave_dirs)
89
107
  if len(all_files) > 1:
90
- all_files.sort(os.path.getmtime)
91
- util.info(f"Choosing: {self.args['file']} (newest)")
108
+ all_files.sort(key=os.path.getmtime)
109
+ util.info(f"Choosing: {all_files[-1]} (newest)")
92
110
  if all_files:
93
111
  wave_file = all_files[-1]
94
112
  else:
@@ -106,17 +124,18 @@ class CommandWaves(CommandDesign):
106
124
  tcl_name = wave_file + '.waves.tcl'
107
125
  with open( tcl_name, 'w', encoding='utf-8') as fo :
108
126
  print( 'current_fileset', file=fo)
109
- print( f'open_wave_database {wave_file=}', file=fo)
127
+ print( f'open_wave_database {wave_file}', file=fo)
110
128
  command_list = [ 'vivado', '-source', tcl_name]
111
129
  self.exec(os.path.dirname(wave_file), command_list)
112
130
  else:
113
131
  self.error(f"Don't know how to open {wave_file} without Vivado in PATH")
114
132
  elif wave_file.endswith('.wlf'):
115
- if 'questa' in self.config['tools_loaded'] and shutil.which('vsim'):
133
+ if self._vsim_available():
116
134
  command_list = ['vsim', wave_file]
117
135
  self.exec(os.path.dirname(wave_file), command_list)
118
136
  else:
119
- self.error(f"Don't know how to open {wave_file} without Questa in PATH")
137
+ self.error(f"Don't know how to open {wave_file} without one of",
138
+ f"{self.VSIM_TOOLS} in PATH")
120
139
  elif wave_file.endswith('.fst'):
121
140
  if 'gtkwave' in self.config['tools_loaded'] and shutil.which('gtkwave'):
122
141
  command_list = ['gtkwave', wave_file]
@@ -124,24 +143,36 @@ class CommandWaves(CommandDesign):
124
143
  else:
125
144
  self.error(f"Don't know how to open {wave_file} without GtkWave in PATH")
126
145
  elif wave_file.endswith('.vcd'):
127
- if 'questa' in self.config['tools_loaded'] and shutil.which('vsim'):
128
- command_list = ['vsim', wave_file]
129
- self.exec(os.path.dirname(wave_file), command_list)
130
- elif 'vivado' in self.config['tools_loaded'] and shutil.which('vivado'):
131
- # I don't think this works, this is a placeholder, I'm sure Vivado can open a VCD
132
- # Also this would be a great place to start adding some open source (GTKWAVE)
133
- # support...
134
- tcl_name = wave_file + '.waves.tcl'
135
- with open( tcl_name, 'w', encoding='utf-8') as fo :
136
- print( 'current_fileset', file=fo)
137
- print( f'open_wave_database {wave_file=}', file=fo)
138
- command_list = [ 'vivado', '-source', tcl_name]
139
- self.exec(os.path.dirname(wave_file), command_list)
140
146
  if 'gtkwave' in self.config['tools_loaded'] and shutil.which('gktwave'):
141
147
  command_list = ['gtkwave', wave_file]
142
148
  self.exec(os.path.dirname(wave_file), command_list)
149
+ elif self._vsim_available(from_tools=self.VSIM_VCD_TOOLS):
150
+ # TODO(drew): untested, may not work, may need to use fst2vcd converter first
151
+ # (from gtkwave install)
152
+ command_list = ['vsim', wave_file]
153
+ self.exec(os.path.dirname(wave_file), command_list)
143
154
  else:
144
155
  self.error(f"Don't know how to open {wave_file} without Vivado,",
145
- "Questa, or gtkwave in PATH")
156
+ f"gtkwave, or {self.VSIM_VCD_TOOLS} in PATH")
146
157
 
147
158
  return tokens
159
+
160
+ def _vsim_available( # pylint: disable=dangerous-default-value
161
+ self, from_tools: list = VSIM_TOOLS
162
+ ) -> bool:
163
+ '''Returns True if 'vsim' is available (Questa or Modelsim)'''
164
+ return bool(shutil.which('vsim')) and \
165
+ any(x in self.config['tools_loaded'] for x in from_tools)
166
+
167
+
168
+ def _test_mode_exec( # pylint: disable=unused-argument
169
+ self, work_dir: str,
170
+ command_list: list,
171
+ **kwargs
172
+ ) -> None:
173
+ '''Override for Command.exec if arg --test-mode was set, does not run
174
+
175
+ the command_list, instead prints to stdout'''
176
+
177
+ util.info(f'waves.py: test_mode exec stdout: {" ".join(command_list)};',
178
+ f' ({work_dir=}')