opencos-eda 0.2.31__tar.gz → 0.2.33__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 (81) hide show
  1. {opencos_eda-0.2.31/opencos_eda.egg-info → opencos_eda-0.2.33}/PKG-INFO +1 -1
  2. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/multi.py +26 -37
  3. opencos_eda-0.2.33/opencos/commands/sweep.py +230 -0
  4. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/deps_helpers.py +3 -2
  5. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/deps_schema.py +40 -11
  6. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_base.py +64 -11
  7. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/peakrdl_cleanup.py +0 -1
  8. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/helpers.py +34 -10
  9. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_deps_helpers.py +6 -5
  10. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_eda.py +65 -41
  11. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_tools.py +11 -2
  12. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/util.py +2 -1
  13. {opencos_eda-0.2.31 → opencos_eda-0.2.33/opencos_eda.egg-info}/PKG-INFO +1 -1
  14. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/pyproject.toml +1 -1
  15. opencos_eda-0.2.31/opencos/commands/sweep.py +0 -170
  16. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/LICENSE +0 -0
  17. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/LICENSE.spdx +0 -0
  18. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/README.md +0 -0
  19. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/__init__.py +0 -0
  20. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/_version.py +0 -0
  21. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/_waves_pkg.sv +0 -0
  22. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/__init__.py +0 -0
  23. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/build.py +0 -0
  24. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/elab.py +0 -0
  25. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/export.py +0 -0
  26. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/flist.py +0 -0
  27. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/open.py +0 -0
  28. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/proj.py +0 -0
  29. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/sim.py +0 -0
  30. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/synth.py +0 -0
  31. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/upload.py +0 -0
  32. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/commands/waves.py +0 -0
  33. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda.py +0 -0
  34. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_config.py +0 -0
  35. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_config_defaults.yml +0 -0
  36. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_config_max_verilator_waivers.yml +0 -0
  37. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_config_reduced.yml +0 -0
  38. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_deps_bash_completion.bash +0 -0
  39. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_extract_deps_keys.py +0 -0
  40. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/eda_tool_helper.py +0 -0
  41. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/export_helper.py +0 -0
  42. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/export_json_convert.py +0 -0
  43. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/files.py +0 -0
  44. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/names.py +0 -0
  45. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/oc_cli.py +0 -0
  46. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/pcie.py +0 -0
  47. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/seed.py +0 -0
  48. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/__init__.py +0 -0
  49. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/custom_config.yml +0 -0
  50. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/deps_files/command_order/DEPS.yml +0 -0
  51. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/deps_files/error_msgs/DEPS.yml +0 -0
  52. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -0
  53. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  54. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -0
  55. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -0
  56. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -0
  57. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_build.py +0 -0
  58. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_deps_schema.py +0 -0
  59. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_eda_elab.py +0 -0
  60. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_eda_synth.py +0 -0
  61. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tests/test_oc_cli.py +0 -0
  62. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/__init__.py +0 -0
  63. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/invio.py +0 -0
  64. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/invio_helpers.py +0 -0
  65. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/invio_yosys.py +0 -0
  66. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/iverilog.py +0 -0
  67. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/modelsim_ase.py +0 -0
  68. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/questa.py +0 -0
  69. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/slang.py +0 -0
  70. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/slang_yosys.py +0 -0
  71. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/surelog.py +0 -0
  72. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/tabbycad_yosys.py +0 -0
  73. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/verilator.py +0 -0
  74. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/vivado.py +0 -0
  75. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos/tools/yosys.py +0 -0
  76. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos_eda.egg-info/SOURCES.txt +0 -0
  77. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos_eda.egg-info/dependency_links.txt +0 -0
  78. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos_eda.egg-info/entry_points.txt +0 -0
  79. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos_eda.egg-info/requires.txt +0 -0
  80. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/opencos_eda.egg-info/top_level.txt +0 -0
  81. {opencos_eda-0.2.31 → opencos_eda-0.2.33}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.31
3
+ Version: 0.2.33
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
@@ -268,26 +268,23 @@ class CommandMulti(CommandParallel):
268
268
  if parsed.parallel < 1 or parsed.parallel > 256:
269
269
  self.error("Arg 'parallel' must be between 1 and 256")
270
270
 
271
- for value in unparsed:
272
- if value in self.config['command_handler'].keys():
273
- command = value
274
- unparsed.remove(value)
275
- break
271
+ command = self.get_command_from_unparsed_args(tokens=unparsed)
276
272
 
277
273
  # Need to know the tool for this command, either it was set correctly via --tool and/or
278
274
  # the command (class) will tell us.
279
275
  all_multi_tools = self.multi_which_tools(command)
280
276
 
277
+ single_cmd_unparsed = self.get_unparsed_args_on_single_command(
278
+ command=command, tokens=unparsed
279
+ )
280
+
281
281
  util.debug(f"Multi: {unparsed=}, looking for target_globs")
282
282
  for token in unparsed:
283
- if token.startswith("-") or token.startswith("+"):
284
- # save all --arg, -arg, or +plusarg for the job target:
285
- arg_tokens.append(token)
286
- else:
283
+ if token in single_cmd_unparsed:
287
284
  target_globs.append(token)
285
+ else:
286
+ arg_tokens.append(token)
288
287
 
289
- if command == "":
290
- self.error("Didn't get a command after 'multi'!")
291
288
 
292
289
  # now we need to expand the target list
293
290
  self.single_command = command
@@ -379,27 +376,23 @@ class CommandMulti(CommandParallel):
379
376
  # Do not use for CommandMulti, b/c we support list of tools.
380
377
  raise NotImplementedError
381
378
 
382
- def multi_which_tools(self, command):
379
+ def multi_which_tools(self, command) -> list:
383
380
  '''returns a list, or None, of the tool that was already determined to run the command
384
381
 
385
382
  CommandToolsMulti will override and return its own list'''
386
383
  return [eda_base.which_tool(command, config=self.config)]
387
384
 
388
- def _append_job_command_args(self, command_list: list) -> None:
389
- # Many args were consumed by eda before CommandMulti saw them
390
- # --config-yml, --tool, --eda-safe. Some are in self.config.
391
- # We need to apply those eda level args to each multi exec-command
392
- # TODO(drew): this is really clunky. It would be way eaiser if our
393
- # self.config['eda_original_args'] had a better way of passing allow-listed
394
- # args from multi --> exec-command.
395
- # Alternatively, we could track all the parsed args up to this point, that have
396
- # deviated from the default value(s), and apply those as needed.
397
- if any(a.startswith('--config-yml') for a in self.config['eda_original_args']):
398
- cfg_yml_fname = self.config.get('config-yml', None)
399
- if cfg_yml_fname:
400
- command_list.append(f'--config-yml={cfg_yml_fname}')
401
- if '--eda-safe' in self.config['eda_original_args']:
402
- command_list.append('--eda-safe')
385
+ def _append_job_command_args( # pylint: disable=R0913,R0917 # too-many-arguments
386
+ self, command_list: list, tool: str, all_multi_tools: list, short_target: str,
387
+ command: str
388
+ ) -> None:
389
+
390
+ super().update_args_list(args=command_list, tool=tool)
391
+ if self.args.get('export-jsonl', False):
392
+ # Special case for 'multi' --export-jsonl, run reach child with --export-json
393
+ command_list.append('--export-json')
394
+ if tool and len(all_multi_tools) > 1:
395
+ command_list.append(f'--sub-work-dir={short_target}.{command}.{tool}')
403
396
 
404
397
  def append_jobs_from_targets(self, args:list):
405
398
  '''Helper method in CommandMulti to apply 'args' (list) to all self.targets,
@@ -422,17 +415,11 @@ class CommandMulti(CommandParallel):
422
415
 
423
416
  _, short_target = os.path.split(target) # trim path info on left
424
417
 
425
- self._append_job_command_args(command_list=command_list)
418
+ self._append_job_command_args(
419
+ command_list=command_list, tool=tool, all_multi_tools=all_multi_tools,
420
+ short_target=short_target, command=command
421
+ )
426
422
 
427
- if tool:
428
- # tool can be None, we won't add it to the command (assumes default from config-yml)
429
- command_list.append('--tool=' + tool)
430
- if len(all_multi_tools) > 1:
431
- command_list += [f'--sub-work-dir={short_target}.{command}.{tool}']
432
-
433
- if self.args.get('export-jsonl', False):
434
- # Special case for 'multi' --export-jsonl, run reach child with --export-json
435
- command_list += [ '--export-json']
436
423
  # if self.args['parallel']: command_list += ['--quiet']
437
424
  command_list += args # put the args prior to the target.
438
425
  command_list += [target]
@@ -540,6 +527,8 @@ class CommandMulti(CommandParallel):
540
527
  output_json_path=output_json_path)
541
528
 
542
529
 
530
+
531
+
543
532
  class CommandToolsMulti(CommandMulti):
544
533
  '''eda.py command handler for: eda tools-multi <args,targets,target-globs,...>
545
534
 
@@ -0,0 +1,230 @@
1
+ '''opencos.commands.sweep - command handler for: eda sweep ...
2
+
3
+ These are not intended to be overriden by child classes. They do not inherit Tool classes.
4
+ '''
5
+
6
+ import os
7
+ import re
8
+
9
+ from opencos import util
10
+ from opencos.eda_base import CommandDesign, CommandParallel, get_eda_exec, which_tool
11
+
12
+ class CommandSweep(CommandDesign, CommandParallel):
13
+ '''Command handler for: eda sweep ...'''
14
+
15
+ command_name = 'sweep'
16
+
17
+ def __init__(self, config:dict):
18
+ CommandDesign.__init__(self, config=config)
19
+ CommandParallel.__init__(self, config=config)
20
+ self.sweep_target = ''
21
+ self.single_command = ''
22
+ self.args.update({
23
+ 'sweep': []
24
+ })
25
+ self.args_help.update({
26
+ "sweep": ("List append arg, where range or value expansion is peformed on the RHS:"
27
+ " --sweep='--arg0=(start,last,iter=1)' "
28
+ " --sweep='--arg1=[val0,val1,val2,...]' "
29
+ " --sweep='+define+NAME0=(start,last,iter=1)' "
30
+ " --sweep='+define+NAME1=[val0,val1,val2,...]' "
31
+ " --sweep='+define+[NAME0,NAME1,NAME2,...]' ."
32
+ " Note that range expansion of (1,4) will expand to values [1,2,3,4]."
33
+ ),
34
+ })
35
+
36
+
37
+ def check_args(self) -> None:
38
+ '''Returns None, checks self.args (use after args parsed)'''
39
+ if self.args['parallel'] < 1 or self.args['parallel'] > 256:
40
+ self.error(f"Arg {self.args['parallel']=} must be between 1 and 256")
41
+
42
+ def _append_sweep_args(self, arg_tokens: list) -> None:
43
+ '''Modifies list arg_tokens, using known top-level args'''
44
+ tool = which_tool(command=self.single_command, config=self.config)
45
+ super().update_args_list(args=arg_tokens, tool=tool)
46
+
47
+
48
+ def process_tokens(
49
+ self, tokens: list, process_all: bool = True,
50
+ pwd: str = os.getcwd()
51
+ ) -> list:
52
+ '''CommandSweep.process_tokens(..) is likely the entry point for: eda sweep <command> ...
53
+
54
+ - handles remaining CLI arguments (tokens list)
55
+ - builds sweep_axis_list to run multiple jobs for the target
56
+ '''
57
+
58
+ # 'sweep' is special in the way it handles tokens, due to most of them being processed by
59
+ # a sub instance
60
+ sweep_axis_list = []
61
+ arg_tokens = []
62
+
63
+ _, unparsed = self.run_argparser_on_list(
64
+ tokens=tokens,
65
+ parser_arg_list=[
66
+ 'parallel',
67
+ 'sweep',
68
+ ],
69
+ apply_parsed_args=True
70
+ )
71
+
72
+ self.check_args()
73
+
74
+ self.single_command = self.get_command_from_unparsed_args(tokens=unparsed)
75
+
76
+ self._append_sweep_args(arg_tokens=arg_tokens)
77
+
78
+ for sweep_arg_value in self.args['sweep']:
79
+ # Deal with --sweep= args we already parsed, but haven't expanded yet.
80
+ sweep_arg_value = util.strip_outer_quotes(sweep_arg_value)
81
+ sweep_axis_list_entry = self._process_sweep_arg(sweep_arg_value=sweep_arg_value)
82
+ if sweep_axis_list_entry:
83
+ sweep_axis_list.append(sweep_axis_list_entry)
84
+
85
+
86
+ # command, --parallel, and --sweep already processed by argparse,
87
+ # let's tentatively parse what the child jobs cannot consume.
88
+ # Whatever is leftover is either an eda target, or another unparsed token.
89
+ single_cmd_unparsed = self.get_unparsed_args_on_single_command(
90
+ command=self.single_command, tokens=unparsed
91
+ )
92
+
93
+ for token in unparsed:
94
+ if token in single_cmd_unparsed:
95
+
96
+ if self.resolve_target(token, no_recursion=True):
97
+ if self.sweep_target:
98
+ self.error("Sweep can only take one target, already got",
99
+ f"{self.sweep_target} now getting {token}")
100
+ self.sweep_target = token
101
+ else:
102
+ # If we don't know what to do with it, pass it to downstream
103
+ arg_tokens.append(token)
104
+
105
+ else:
106
+ # If it wasn't in single_cmd_unparsed, then it can definitely be
107
+ # consumed downstream
108
+ arg_tokens.append(token)
109
+
110
+
111
+ # now we need to expand the target list
112
+ util.debug(f"Sweep: command: '{self.single_command}'")
113
+ util.debug(f"Sweep: arg_tokens: '{arg_tokens}'")
114
+ util.debug(f"Sweep: target: '{self.sweep_target}'")
115
+
116
+ # now create the list of jobs, support one axis
117
+ self.jobs = []
118
+
119
+ self.expand_sweep_axis(arg_tokens=arg_tokens, sweep_axis_list=sweep_axis_list)
120
+ self.run_jobs(command=self.single_command)
121
+ return [] # we used all of unparsed.
122
+
123
+
124
+ @staticmethod
125
+ def _process_sweep_arg(sweep_arg_value: str) -> dict:
126
+ '''If processed, returns a non-empty dict. Handles processing of --sweep=VALUE
127
+
128
+ Where VALUE could be one of:
129
+ --arg0=(1,4)
130
+ --arg1=[val0,val1]
131
+ +define+NAME0=[val0,val1]
132
+
133
+ Return value is {} or {'lhs': str, 'operator': str (+ or =), 'values': list}
134
+ '''
135
+
136
+ sweep_arg_value = util.strip_outer_quotes(sweep_arg_value)
137
+
138
+ util.debug(f'{sweep_arg_value=}')
139
+ # Try to match a sweep range expansion:
140
+ # --sweep='--arg0=(1,4)'
141
+ # --sweep='--arg0=(1,4,1)'
142
+ # --sweep='+define+NAME0=(1,4,1)'
143
+ m = re.match(
144
+ r'(.*)(\=) \( ([\d\.]+) \, ([\d\.]+) (\, ([\d\.]+) )? \)'.replace(' ',''),
145
+ sweep_arg_value
146
+ )
147
+ if m:
148
+ lhs = m.group(1)
149
+ operator = m.group(2)
150
+ sweep_axis_values = []
151
+ if m.group(5):
152
+ rhs_range_iter = int(m.group(6))
153
+ else:
154
+ rhs_range_iter = 1
155
+ for v in range(int(m.group(3)), int(m.group(4)) + 1, rhs_range_iter):
156
+ sweep_axis_values.append(v)
157
+ util.debug(f"Sweep axis: {lhs} {operator} {sweep_axis_values}")
158
+ return {
159
+ 'lhs': lhs,
160
+ 'operator': operator,
161
+ 'values': sweep_axis_values,
162
+ }
163
+
164
+ # Try to match a sweep list value expansion:
165
+ # --sweep='--arg0=[1,2]'
166
+ # --sweep='--arg1=[3,4,5,6]'
167
+ # --sweep='+define+NAME=[8,9]'
168
+ # --sweep='+define+[NAME0,NAME1,NAME2]'
169
+ # --sweep='+define+[NAME0=1,NAME1=22,NAME2=46]'
170
+ m = re.match(
171
+ r'(.*)([\=\+]) \[ ([^\]]+) \] '.replace(' ',''),
172
+ sweep_arg_value
173
+ )
174
+ if m:
175
+ sweep_axis_values = []
176
+ lhs = m.group(1)
177
+ operator = m.group(2)
178
+ for v in m.group(3).split(','):
179
+ sweep_axis_values.append(v)
180
+ util.debug(f"Sweep axis: {lhs} {operator} {sweep_axis_values}")
181
+ return {
182
+ 'lhs': lhs,
183
+ 'operator': operator,
184
+ 'values': sweep_axis_values,
185
+ }
186
+
187
+ util.warning(f'Ignored unprocessed expansion for --sweep={sweep_arg_value}')
188
+ return {}
189
+
190
+
191
+ def expand_sweep_axis(
192
+ self, arg_tokens: list, sweep_axis_list: list, sweep_string: str = ""
193
+ ) -> None:
194
+ '''Returns None, appends jobs to self.jobs to be run by CommandParallel.run_jobs(..)'''
195
+
196
+ util.debug(f"Entering expand_sweep_axis: command={self.single_command},",
197
+ f"target={self.sweep_target}, arg_tokens={arg_tokens},",
198
+ f"sweep_axis_list={sweep_axis_list}")
199
+ if not sweep_axis_list:
200
+ # we aren't sweeping anything, create one job
201
+ snapshot_name = self.sweep_target.replace('../','').replace('/','_') + sweep_string
202
+ eda_path = get_eda_exec('sweep')
203
+ self.jobs.append({
204
+ 'name' : snapshot_name,
205
+ 'index' : len(self.jobs),
206
+ 'command_list' : (
207
+ [eda_path, self.single_command, self.sweep_target,
208
+ '--job_name', snapshot_name] + arg_tokens
209
+ )
210
+ })
211
+ return
212
+
213
+ sweep_axis = sweep_axis_list.pop(0)
214
+ lhs = sweep_axis['lhs']
215
+ operator = sweep_axis['operator']
216
+
217
+ lhs_trimmed = lhs.replace('-', '').replace('+', '').replace('=', '')
218
+
219
+ for v in sweep_axis['values']:
220
+ this_arg_tokens = arg_tokens.copy()
221
+ this_arg_tokens.append(f'{lhs}{operator}{v}')
222
+
223
+ v_string = f"{v}".replace('.','p')
224
+ this_sweep_string = sweep_string + f"_{lhs_trimmed}_{v_string}"
225
+
226
+ self.expand_sweep_axis(
227
+ arg_tokens=this_arg_tokens,
228
+ sweep_axis_list=sweep_axis_list,
229
+ sweep_string=this_sweep_string
230
+ )
@@ -1,6 +1,7 @@
1
1
 
2
2
  import fnmatch
3
3
  import os
4
+ from pathlib import Path
4
5
  import sys
5
6
  import re
6
7
  import shutil
@@ -1149,9 +1150,9 @@ def parse_deps_peakrdl(line : str, target_path : str, target_node : str, enable
1149
1150
 
1150
1151
 
1151
1152
  shell_commands = [
1152
- [ 'peakrdl', 'regblock', '-o', 'peakrdl/'] + args_list,
1153
+ [ 'peakrdl', 'regblock', '-o', str(Path('peakrdl/'))] + args_list,
1153
1154
  # Edit file to apply some verilator waivers, etc, from peakrdl_cleanup.py:
1154
- [ 'python3', peakrdl_cleanup_py, f'peakrdl/{top}.sv', f'peakrdl/{top}.sv' ],
1155
+ [ 'python3', peakrdl_cleanup_py, str(Path(f'peakrdl/{top}.sv')), str(Path(f'peakrdl/{top}.sv')) ],
1155
1156
  ]
1156
1157
 
1157
1158
  ret_dict = {
@@ -125,7 +125,7 @@ import sys
125
125
 
126
126
  from schema import Schema, Or, Optional, SchemaError
127
127
 
128
- from opencos.deps_helpers import deps_markup_safe_load, get_deps_markup_file
128
+ from opencos import util, deps_helpers
129
129
 
130
130
  # Because we deal with YAML, where a Table Key with dangling/empty value is allowed
131
131
  # and we have things like SystemVerilog defines where there's a Table key with no Value,
@@ -299,8 +299,10 @@ FILE_SIMPLIFIED = Schema(
299
299
 
300
300
 
301
301
 
302
- def check(data: dict, schema_obj=FILE):
302
+ def check(data: dict, schema_obj=FILE) -> (bool, str):
303
303
  '''Returns (bool, str) for checking dict against FILE schema'''
304
+
305
+
304
306
  try:
305
307
  schema_obj.validate(data)
306
308
  return True, None
@@ -310,6 +312,37 @@ def check(data: dict, schema_obj=FILE):
310
312
  return False, str(e)
311
313
 
312
314
 
315
+ def deps_markup_safe_load(deps_filepath: str) -> (bool, dict):
316
+ '''Returns tuple (bool False if took errors, dict of markp data)'''
317
+ current_errors = util.args['errors']
318
+ data = deps_helpers.deps_markup_safe_load(deps_filepath)
319
+ if util.args['errors'] > current_errors:
320
+ return False, data
321
+ return True, data
322
+
323
+
324
+ def check_file(filepath: str, schema_obj=FILE) -> (bool, str, str):
325
+ '''Returns tuple (bool pass/fail, str error retdata, str deps_filepath)'''
326
+
327
+ deps_filepath = filepath
328
+ if os.path.isdir(filepath):
329
+ deps_filepath = deps_helpers.get_deps_markup_file(base_path=filepath)
330
+
331
+ # get deps file
332
+ if not os.path.exists(deps_filepath):
333
+ print(f'ERROR: internal error(s) no DEPS.[yml|..] found in {filepath=}')
334
+ return False, '', deps_filepath
335
+
336
+ passes, data = deps_markup_safe_load(deps_filepath)
337
+ if not passes:
338
+ print(f'ERROR: internal error(s) from deps_markup_safe_load({deps_filepath=})')
339
+ return False, '', deps_filepath
340
+
341
+ passes, retdata = check(data, schema_obj)
342
+ return passes, retdata, deps_filepath
343
+
344
+
345
+
313
346
  def check_files(files, schema_obj=FILE) -> bool:
314
347
  '''Returns True if files lint cleanly in the FILE schema.'''
315
348
 
@@ -319,20 +352,16 @@ def check_files(files, schema_obj=FILE) -> bool:
319
352
  passes_list = []
320
353
  error_files = []
321
354
  for filepath in files:
322
- deps_filepath = filepath
323
- if os.path.isdir(filepath):
324
- # get deps file
325
- deps_filepath = get_deps_markup_file(base_path=filepath)
326
- assert os.path.exists(deps_filepath)
327
- data = deps_markup_safe_load(deps_filepath)
328
- passes, retdata = check(data, schema_obj)
355
+
356
+ passes, retdata, deps_filepath = check_file(filepath, schema_obj)
329
357
  passes_list.append(passes)
330
358
  if passes:
331
359
  print(f'{deps_filepath}: [PASS]')
332
360
  if not passes:
333
361
  print(f'ERROR: {deps_filepath}:')
334
- print('-- retdata --')
335
- print(retdata)
362
+ if retdata:
363
+ print('-- retdata --')
364
+ print(retdata)
336
365
  print(f' previous error on: {deps_filepath}\n')
337
366
  error_files.append(deps_filepath)
338
367
 
@@ -398,12 +398,6 @@ class Command:
398
398
  else:
399
399
  assert False, f'{key=} {value=} how do we do argparse for this type of value?'
400
400
 
401
- # TODO(drew): it might be nice to support positional args here as a list
402
- # self.target_args (files/targets/patterns), something like:
403
- # parser.add_argument(
404
- # 'targets', nargs='+', help='positional arg for targets/files/pattern'
405
- # )
406
-
407
401
  return parser
408
402
 
409
403
 
@@ -425,7 +419,7 @@ class Command:
425
419
  parsed, unparsed = parser.parse_known_args(tokens + [''])
426
420
  unparsed = list(filter(None, unparsed))
427
421
  except argparse.ArgumentError:
428
- self.error(f'problem attempting to parse_known_args for {tokens=}')
422
+ self.error(f'problem {command_name=} attempting to parse_known_args for {tokens=}')
429
423
 
430
424
  parsed_as_dict = vars(parsed)
431
425
 
@@ -506,7 +500,8 @@ class Command:
506
500
  break
507
501
 
508
502
  if not ret and error_if_no_command:
509
- self.error(f"Looking for a valid eda <command> but didn't find one in {tokens=}")
503
+ self.error(f"Looking for a valid eda {self.command_name} <command>",
504
+ f"but didn't find one in {tokens=}")
510
505
  return ret
511
506
 
512
507
 
@@ -696,8 +691,12 @@ class CommandDesign(Command):
696
691
  util.info(f'run_dep_shell_commands {iter=}: {d=}')
697
692
  clist = util.ShellCommandList(d['exec_list'])
698
693
  # NOTE(drew): shell=True subprocess call, can disable with self.config
699
- self.exec(self.args['work-dir'], clist, tee_fpath=clist.tee_fpath,
700
- shell=self.config.get('deps_subprocess_shell', False))
694
+ # However, in Windows, we seem to have to run these with shell=False
695
+ if sys.platform.startswith('win'):
696
+ self.exec(self.args['work-dir'], clist, shell=False)
697
+ else:
698
+ self.exec(self.args['work-dir'], clist, tee_fpath=clist.tee_fpath,
699
+ shell=self.config.get('deps_subprocess_shell', False))
701
700
 
702
701
  def update_file_lists_for_work_dir(self):
703
702
  if len(self.dep_work_dir_add_srcs) == 0:
@@ -748,7 +747,7 @@ class CommandDesign(Command):
748
747
  to unprocessed-plusargs.
749
748
  '''
750
749
 
751
- # Since this may be coming from a raw CLI/bash argparser, we may have
750
+ # Since this may be coming from a raw CLI/bash/powershell argparser, we may have
752
751
  # args that come from shlex.quote(token), such as:
753
752
  # token = '\'+define+OC_ROOT="/foo/bar/opencos"\''
754
753
  # So we strip all outer ' or " on the plusarg:
@@ -1514,3 +1513,57 @@ class CommandParallel(Command):
1514
1513
  if self.status == 0:
1515
1514
  self.status = 0 if len(self.jobs_status) == 0 else max(self.jobs_status)
1516
1515
  util.fancy_stop()
1516
+
1517
+
1518
+ def update_args_list(self, args: list, tool: str) -> None:
1519
+ '''Modfies list args, using allow-listed known top-level args:
1520
+
1521
+ --config-yml
1522
+ --eda-safe
1523
+ --tool
1524
+
1525
+ Many args were consumed by eda before CommandParallel saw them
1526
+ (for commands like 'multi' or 'sweep'). Some are in self.config.
1527
+ We need to apply those eda level args to each single exec-command
1528
+ '''
1529
+ if any(a.startswith('--config-yml') for a in self.config['eda_original_args']):
1530
+ cfg_yml_fname = self.config.get('config-yml', None)
1531
+ if cfg_yml_fname:
1532
+ args.append(f'--config-yml={cfg_yml_fname}')
1533
+ if '--eda-safe' in self.config['eda_original_args']:
1534
+ args.append('--eda-safe')
1535
+ if tool:
1536
+ # tool can be None, if so we won't add it to the command (assumes default from
1537
+ # config-yml auto load order)
1538
+ args.append('--tool=' + tool)
1539
+
1540
+
1541
+ def get_unparsed_args_on_single_command(self, command: str, tokens: list) -> list:
1542
+ '''Returns a list of args that the single (non-multi) command cannot parse
1543
+
1544
+ This will error on bad --args or -arg, such as:
1545
+ eda multi sim --seeeed=1
1546
+ is not a valid arg in CommandSim
1547
+
1548
+ +arg=value, +arg+value will not be included in the return list, those are
1549
+ intended to be consumed by the single/job command downstream (anything starting
1550
+ with '+')
1551
+
1552
+ Used by CommandMulti and CommandSweep.
1553
+ '''
1554
+ single_cmd_handler = self.config['command_handler'][command](config=self.config)
1555
+ single_cmd_parsed, single_cmd_unparsed = single_cmd_handler.run_argparser_on_list(
1556
+ tokens=tokens.copy(),
1557
+ apply_parsed_args=False,
1558
+ )
1559
+ util.debug(f'{self.command_name}: {single_cmd_unparsed=}')
1560
+
1561
+ # There should not be any single_cmd_unparsed args starting with '-'
1562
+ bad_remaining_args = [x for x in single_cmd_unparsed if x.startswith('-')]
1563
+ if bad_remaining_args:
1564
+ self.error(f'for {self.command_name} {command=} the following args are unknown',
1565
+ f'{bad_remaining_args}')
1566
+
1567
+ # Remove unparsed args starting with '+', since those are commonly sent downstream to
1568
+ # single job (example, CommandSim plusargs).
1569
+ return [x for x in single_cmd_unparsed if not x.startswith('+')]
@@ -2,7 +2,6 @@
2
2
 
3
3
  import sys
4
4
  import os
5
- import subprocess
6
5
 
7
6
  def run(file_in, file_out):
8
7
  with open(file_in) as f:
@@ -11,6 +11,19 @@ from contextlib import redirect_stdout, redirect_stderr
11
11
  from opencos import eda, util
12
12
  from opencos import deps_schema
13
13
 
14
+ def can_run_eda_command(*commands, config: dict) -> bool:
15
+ '''Returns True if we have any installed tool that can run: eda <command>'''
16
+ runnable = []
17
+ for command in list(commands):
18
+ handler = config['command_handler'].get(command, None)
19
+ if not handler:
20
+ return False
21
+ if handler and getattr(handler, 'CHECK_REQUIRES', []):
22
+ if not all(isinstance(handler, x) for x in getattr(handler, 'CHECK_REQUIRES', [])):
23
+ return False
24
+ runnable.append(True)
25
+ return runnable and all(runnable)
26
+
14
27
  def chdir_remove_work_dir(startpath, relpath):
15
28
  '''Changes dir to startpath/relpath, removes the work directories (eda.work, eda.export*)'''
16
29
  os.chdir(os.path.join(startpath, relpath))
@@ -139,41 +152,52 @@ class Helpers:
139
152
  print(f'Wrote: {os.path.abspath(logfile)=}')
140
153
  return rc
141
154
 
142
- def is_in_log(self, *want_str, logfile=None):
155
+ def is_in_log(self, *want_str, logfile=None, windows_path_support=False):
143
156
  '''Check if any of want_str args are in the logfile, or self.DEFAULT_LOG'''
144
157
  if logfile is None:
145
158
  logfile = self.DEFAULT_LOG
146
- want_str = ' '.join(list(want_str))
159
+ want_str0 = ' '.join(list(want_str))
160
+ want_str1 = want_str0.replace('/', '\\')
147
161
  with open(logfile, encoding='utf-8') as f:
148
162
  for line in f.readlines():
149
- if want_str in line:
163
+ if want_str0 in line or \
164
+ (windows_path_support and want_str1 in line):
150
165
  return True
151
166
  return False
152
167
 
153
- def get_log_lines_with(self, *want_str, logfile=None):
168
+ def get_log_lines_with(self, *want_str, logfile=None, windows_path_support=False):
154
169
  '''gets all log lines with any of want_str args are in the logfile, or self.DEFAULT_LOG'''
155
170
  if logfile is None:
156
171
  logfile = self.DEFAULT_LOG
157
172
  ret_list = []
158
- want_str = ' '.join(list(want_str))
173
+ want_str0 = ' '.join(list(want_str))
174
+ want_str1 = want_str0.replace('/', '\\')
159
175
  with open(logfile, encoding='utf-8') as f:
160
176
  for line in f.readlines():
161
- if want_str in line:
177
+ if want_str0 in line:
178
+ ret_list.append(line)
179
+ elif windows_path_support and want_str1 in line:
162
180
  ret_list.append(line)
163
181
  return ret_list
164
182
 
165
- def get_log_words_with(self, *want_str, logfile=None):
183
+ def get_log_words_with(self, *want_str, logfile=None, windows_path_support=False):
166
184
  '''gets all log lines with any of *want_str within a single word
167
185
  in the logfile or self.DEFAULT_LOG
168
186
  '''
169
187
  if logfile is None:
170
188
  logfile = self.DEFAULT_LOG
171
189
  ret_list = []
172
- want_str = ' '.join(list(want_str))
190
+ want_str0 = ' '.join(list(want_str))
191
+ want_str1 = want_str0.replace('/', '\\')
173
192
  with open(logfile, encoding='utf-8') as f:
174
193
  for line in f.readlines():
175
- if want_str in line:
194
+ if want_str0 in line:
176
195
  for word in line.split():
177
- if want_str in word:
196
+ if want_str0 in word:
178
197
  ret_list.append(word)
198
+ elif windows_path_support and want_str1 in line:
199
+ for word in line.split():
200
+ if want_str1 in word:
201
+ ret_list.append(word)
202
+
179
203
  return ret_list