opencos-eda 0.2.52__py3-none-any.whl → 0.2.53__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 (41) hide show
  1. opencos/commands/__init__.py +2 -0
  2. opencos/commands/build.py +1 -1
  3. opencos/commands/deps_help.py +259 -0
  4. opencos/commands/export.py +1 -1
  5. opencos/commands/flist.py +3 -0
  6. opencos/commands/lec.py +1 -1
  7. opencos/commands/open.py +2 -0
  8. opencos/commands/proj.py +1 -1
  9. opencos/commands/shell.py +1 -1
  10. opencos/commands/sim.py +32 -8
  11. opencos/commands/synth.py +1 -1
  12. opencos/commands/upload.py +3 -0
  13. opencos/commands/waves.py +1 -0
  14. opencos/deps/defaults.py +1 -0
  15. opencos/deps/deps_file.py +30 -4
  16. opencos/deps/deps_processor.py +72 -2
  17. opencos/deps_schema.py +3 -0
  18. opencos/eda.py +50 -26
  19. opencos/eda_base.py +159 -20
  20. opencos/eda_config.py +1 -1
  21. opencos/eda_config_defaults.yml +49 -3
  22. opencos/eda_extract_targets.py +1 -58
  23. opencos/tests/helpers.py +16 -0
  24. opencos/tests/test_eda.py +13 -2
  25. opencos/tests/test_tools.py +159 -132
  26. opencos/tools/cocotb.py +9 -0
  27. opencos/tools/modelsim_ase.py +67 -51
  28. opencos/tools/quartus.py +638 -0
  29. opencos/tools/questa.py +167 -88
  30. opencos/tools/questa_fse.py +10 -0
  31. opencos/tools/riviera.py +1 -0
  32. opencos/tools/vivado.py +3 -3
  33. opencos/util.py +20 -3
  34. opencos/utils/str_helpers.py +85 -0
  35. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/METADATA +1 -1
  36. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/RECORD +41 -39
  37. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/WHEEL +0 -0
  38. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/entry_points.txt +0 -0
  39. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE +0 -0
  40. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE.spdx +0 -0
  41. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/top_level.txt +0 -0
@@ -103,6 +103,37 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
103
103
  self.command_design_ref.incdirs.append(abspath)
104
104
  debug(f'Added include dir {abspath} from {self.caller_info}')
105
105
 
106
+
107
+ def _apply_args_check_tools(self, tokens: list) -> list:
108
+ '''Helper for apply_args(list), returns list strips --tool args under certain conditions'''
109
+
110
+ _orig_tool = self.args.get('tool', '')
111
+ if not self.command_design_ref.auto_tool_applied and \
112
+ any(x.startswith('--tool') for x in tokens) and _orig_tool:
113
+ warn_tool = ''
114
+ for i, item in enumerate(list(tokens)):
115
+ if item == '--tool':
116
+ if tokens[i + 1] != _orig_tool:
117
+ warn_tool = tokens[i + 1]
118
+ tokens[i : i+2] = ['', ''] # remove this and next arg
119
+ elif item.startswith('--tool='):
120
+ if item[7:] != _orig_tool:
121
+ warn_tool = item
122
+ tokens[i] = '' # remove just this arg.
123
+
124
+ if warn_tool:
125
+ warning(
126
+ f'Attempting to set --tool {warn_tool} from DEPS',
127
+ f'(file={self.deps_file}:{self.target_node})',
128
+ f'however the tool was already chosen as: {_orig_tool}. The --tool arg will',
129
+ f'not be applied from: {tokens}'
130
+ )
131
+
132
+ tokens = [item for item in tokens if item != ''] # remove blanks
133
+
134
+ return tokens
135
+
136
+
106
137
  def apply_args(self, args_list:list) -> list:
107
138
  '''Given args_list, applies them to our self.command_design_ref obj
108
139
 
@@ -114,9 +145,20 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
114
145
  self.error(f"{args_list=} is not type str/list, can't apply args",
115
146
  f"in {self.caller_info}")
116
147
  tokens = dep_str2list(args_list)
148
+
117
149
  # We're going to run an ArgumentParser here, which is not the most efficient
118
150
  # thing to do b/c it runs on all of self.command_design_ref.args (dict) even
119
151
  # if we're applying a single token.
152
+
153
+ # We have to special-case anything with --tool[=value] in tokens, otherwise
154
+ # the user may think they were allowed to set --tool, but in our flow the Command handler
155
+ # (self.command_design_ref) has already been chosen, so setting the tool can have
156
+ # strange side-effects.
157
+ _orig_tool = self.args.get('tool', '')
158
+ tokens = self._apply_args_check_tools(tokens=tokens)
159
+ if not tokens:
160
+ return []
161
+
120
162
  debug(f'deps_processor - custom apply_args with {tokens=}',
121
163
  f'from {self.caller_info}')
122
164
  _, unparsed = self.command_design_ref.run_argparser_on_list(
@@ -135,6 +177,16 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
135
177
  # flist from that target.
136
178
  warning(f'For {self.command_design_ref.command_name}:' \
137
179
  + f' in {self.caller_info} has unknown args {unparsed=}')
180
+
181
+ if self.command_design_ref.auto_tool_applied and _orig_tool != self.args.get('tool', ''):
182
+ debug(f'deps_processor.apply_args: tool changed, {self.args["tool"]=}, will attempt',
183
+ f'to respawn the job using original args: {self.config["eda_original_args"]}')
184
+ self.command_design_ref.tool_changed_respawn = {
185
+ 'tool': self.args['tool'],
186
+ 'orig_tool': _orig_tool,
187
+ 'from': self.caller_info,
188
+ }
189
+
138
190
  return unparsed
139
191
 
140
192
  def apply_reqs(self, reqs_list:list) -> None:
@@ -213,6 +265,10 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
213
265
  elif key == 'deps':
214
266
  remaining_deps_list += self.process_deps_return_discovered_deps()
215
267
 
268
+ if self.command_design_ref.tool_changed_respawn:
269
+ # Stop now, and have eda.py respawn the command.
270
+ return []
271
+
216
272
  # We return the list of deps that still need to be resolved (['full_path/some_target', ...])
217
273
  return remaining_deps_list
218
274
 
@@ -260,6 +316,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
260
316
 
261
317
 
262
318
  apply_tag_items_tools = False
319
+ apply_tag_items_commands = False
263
320
  apply_tag_items_with_args = False
264
321
 
265
322
  tool = self.args.get('tool', None)
@@ -276,6 +333,12 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
276
333
  warning(f'{tagname=} in {self.caller_info}:',
277
334
  ' skipped due to with-tools disabled.')
278
335
 
336
+ with_commands = dep_str2list(value.get('with-commands', []))
337
+ if with_commands and not deps_tags_enables.get('with-commands', None):
338
+ with_commands = []
339
+ warning(f'{tagname=} in {self.caller_info}:',
340
+ ' skipped due to with-commands disabled.')
341
+
279
342
  with_args = value.get('with-args', {})
280
343
  if not isinstance(with_args, dict):
281
344
  error(f'{tagname=} in {self.caller_info}:',
@@ -299,6 +362,12 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
299
362
  if tool_full_version and tool_full_version in with_tools:
300
363
  apply_tag_items_tools = True
301
364
 
365
+ # check with-commands?
366
+ if not with_commands:
367
+ apply_tag_items_commands = True # no with-commands present
368
+ elif getattr(self.command_design_ref, 'command_name', '') in with_commands:
369
+ apply_tag_items_commands = True # with-commands present and we matched.
370
+
302
371
  # check with-args?
303
372
  with_args_matched_list = []
304
373
  for k,v in with_args.items():
@@ -336,7 +405,8 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
336
405
  elif enable_tags_matched:
337
406
  apply_tag_items = True
338
407
  else:
339
- apply_tag_items = apply_tag_items_tools and apply_tag_items_with_args
408
+ apply_tag_items = all([apply_tag_items_tools, apply_tag_items_with_args,
409
+ apply_tag_items_commands])
340
410
 
341
411
  if not apply_tag_items:
342
412
  debug(f'process_tags(): {tagname=} in {self.caller_info}',
@@ -370,7 +440,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
370
440
  if args_list:
371
441
  # This will apply knowns args to the target dep:
372
442
  info(f'{tagname=} in {self.caller_info=}:',
373
- f'applying args b/c {with_tools=} for {args_list=}')
443
+ f'applying args for {args_list=}')
374
444
  self.apply_args(args_list)
375
445
 
376
446
  elif key == 'reqs':
opencos/deps_schema.py CHANGED
@@ -111,6 +111,8 @@ my_target_name:
111
111
  <str>: <---- string user name for this tag
112
112
  with-tools: <---- optional array (or string) of tools this tag requires
113
113
  for the tag to be applied.
114
+ with-commands: <---- optional array (or string) of command this tag requires
115
+ for the tag to be applied.
114
116
  with-args: <---- optional table of args values that must match for
115
117
  this tag to be applied.
116
118
  args: <---- optional array of args to be applied to this target
@@ -217,6 +219,7 @@ TARGET_EDA_COMMAND = {
217
219
  TARGET_TAGS_TABLE = {
218
220
  Optional(str): {
219
221
  Optional('with-tools'): ARRAY_OR_SPACE_SEPARATED_STRING,
222
+ Optional('with-commands'): ARRAY_OR_SPACE_SEPARATED_STRING,
220
223
  Optional('with-args'): dict,
221
224
  Optional('args'): ARRAY_OR_SPACE_SEPARATED_STRING,
222
225
  Optional('deps'): ARRAY_OR_SPACE_SEPARATED_STRING,
opencos/eda.py CHANGED
@@ -20,13 +20,19 @@ from pathlib import Path
20
20
 
21
21
  import opencos
22
22
  from opencos import util, eda_config, eda_base
23
- from opencos.eda_base import Tool, which_tool
23
+ from opencos.eda_base import Tool, which_tool, get_eda_exec
24
24
  from opencos.utils import vsim_helper, vscode_helper
25
+ from opencos.utils.subprocess_helpers import subprocess_run_background
26
+ from opencos.utils import status_constants
25
27
 
26
28
  # Configure util:
27
29
  util.progname = "EDA"
28
30
  util.global_log.default_log_enabled = True
29
31
  util.global_log.default_log_filepath = os.path.join('eda.work', 'eda.log')
32
+ util.global_log.default_log_disable_with_args.extend([
33
+ # avoid default log on certain eda commands
34
+ 'help', 'waves', 'deps-help', 'targets'
35
+ ])
30
36
 
31
37
 
32
38
  # ******************************************************************************
@@ -44,12 +50,12 @@ def init_config(
44
50
  # For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
45
51
  # the actual class using importlib (via opencos.util)
46
52
  config['command_handler'] = {}
47
- for command, str_class in config['DEFAULT_HANDLERS'].items():
53
+ for _cmd, str_class in config['DEFAULT_HANDLERS'].items():
48
54
  cls = util.import_class_from_string(str_class)
49
55
  if not cls:
50
- util.error(f"config DEFAULT_HANDLERS {command=} {str_class=} could not import")
56
+ util.error(f"config DEFAULT_HANDLERS command={_cmd} {str_class=} could not import")
51
57
  else:
52
- config['command_handler'][command] = cls
58
+ config['command_handler'][_cmd] = cls
53
59
 
54
60
  config['auto_tools_found'] = {}
55
61
  config['tools_loaded'] = set()
@@ -58,6 +64,18 @@ def init_config(
58
64
  return config
59
65
 
60
66
 
67
+ def get_all_commands_help_str(config: dict) -> str:
68
+ '''Returns a str of help based on what commands eda supports, from config'''
69
+ all_commands_help = []
70
+ max_command_str_len = max(len(s) for s in config.get('DEFAULT_HANDLERS_HELP', {}).keys())
71
+ for key, value in config.get('DEFAULT_HANDLERS_HELP', {}).items():
72
+ all_commands_help.append(f' {key:<{max_command_str_len}} - {value.strip()}')
73
+ if all_commands_help:
74
+ all_commands_help = [
75
+ 'Where <command> is one of:',
76
+ '',
77
+ ] + all_commands_help
78
+ return '\n'.join(all_commands_help)
61
79
 
62
80
 
63
81
  def usage(tokens: list, config: dict, command="") -> int:
@@ -77,26 +95,11 @@ def usage(tokens: list, config: dict, command="") -> int:
77
95
  """
78
96
  Usage:
79
97
  eda [<options>] <command> [options] <files|targets, ...>
80
-
81
- Where <command> is one of:
82
-
83
- sim - Simulates a DEPS target
84
- elab - Elaborates a DEPS target (sort of sim based LINT)
85
- synth - Synthesizes a DEPS target
86
- flist - Create dependency from a DEPS target
87
- proj - Create a project from a DEPS target for GUI sim/waves/debug
88
- multi - Run multiple DEPS targets, serially or in parallel
89
- tools-multi - Same as 'multi' but run on all available tools, or specfied using --tools
90
- sweep - Sweep one or more arguments across a range, serially or in parallel
91
- build - Build for a board, creating a project and running build flow
92
- waves - Opens waveform from prior simulation
93
- upload - Uploads a finished design into hardware
94
- open - Opens a project
95
- export - Export files related to a target, tool independent
96
- shell - Runs only commands for DEPS target (like sim or elab, but stops prior to tool)
97
- targets - list all possible targets given glob path.
98
- help - This help (without args), or i.e. "eda help sim" for specific help
99
-
98
+ """
99
+ )
100
+ print(get_all_commands_help_str(config=config))
101
+ print(
102
+ """
100
103
  And <files|targets, ...> is one or more source file or DEPS markup file target,
101
104
  such as .v, .sv, .vhd[l], .cpp files, or a target key in a DEPS.[yml|yaml|toml|json].
102
105
  Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` to
@@ -249,7 +252,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
249
252
  p = shutil.which(exe)
250
253
  config['auto_tools_found'][name] = exe # populate key-value pairs w/ first exe in list
251
254
  if not quiet:
252
- util.info(f"Detected {name} ({p}), auto-setting up tool {name}")
255
+ util.info(f"Detected {name} ({p})")
253
256
  tool_setup(tool=name, quiet=True, auto_setup=True, warnings=warnings, config=config)
254
257
 
255
258
  return config
@@ -317,7 +320,7 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
317
320
  config['tools_loaded'].add(tool)
318
321
 
319
322
 
320
- def process_tokens( # pylint: disable=too-many-branches,too-many-statements
323
+ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-many-locals
321
324
  tokens: list, original_args: list, config: dict, is_interactive=False
322
325
  ) -> int:
323
326
  '''Returns bash/sh style return code int (0 pass, non-zero fail).
@@ -335,6 +338,7 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements
335
338
  deferred_tokens = []
336
339
  command = ""
337
340
  run_auto_tool_setup = True
341
+ process_tokens_cwd = os.getcwd()
338
342
 
339
343
  parser = eda_base.get_argparser()
340
344
  try:
@@ -409,6 +413,7 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements
409
413
  command not in config.get('command_tool_is_optional', []):
410
414
  use_tool = which_tool(command, config)
411
415
  util.info(f"--tool not specified, using default for {command=}: {use_tool}")
416
+ setattr(sco, 'auto_tool_applied', True)
412
417
 
413
418
  rc = check_command_handler_cls(command_obj=sco, command=command, parsed_args=parsed)
414
419
  if rc > 0:
@@ -426,6 +431,25 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements
426
431
  # rc=1 because that's the python exception rc)
427
432
  rc = getattr(sco, 'status', 2)
428
433
  util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
434
+
435
+ if rc == 0 and not parsed.tool and getattr(sco, 'tool_changed_respawn', False):
436
+ use_tool = sco.args.get('tool', '')
437
+ if not use_tool:
438
+ util.error(f'Unable to change tool from {parsed.tool}, internal eda problem.')
439
+ return status_constants.EDA_DEFAULT_ERROR
440
+
441
+ # close the util.log:
442
+ util.stop_log()
443
+ # respawn the original job, but with --tool=<use_tool> applied:
444
+ _command_list = [get_eda_exec(command), f"--tool={use_tool}"] + original_args
445
+ util.info(f'eda: respawn for tool change: {" ".join(_command_list)};',
446
+ f' (running from: {process_tokens_cwd})')
447
+ subprocess_run_background(
448
+ work_dir=process_tokens_cwd,
449
+ command_list=_command_list,
450
+ background=util.args.get('quiet', False)
451
+ )
452
+
429
453
  return rc
430
454
 
431
455
 
opencos/eda_base.py CHANGED
@@ -26,7 +26,7 @@ from opencos import eda_config
26
26
 
27
27
  from opencos.util import Colors
28
28
  from opencos.utils.str_helpers import sprint_time, strip_outer_quotes, string_or_space, \
29
- indent_wrap_long_text
29
+ indent_wrap_long_text, pretty_list_columns_manual
30
30
  from opencos.utils.subprocess_helpers import subprocess_run_background
31
31
  from opencos.utils import status_constants
32
32
 
@@ -68,6 +68,17 @@ def get_argparser_short_help() -> str:
68
68
  return util.get_argparser_short_help(parser=get_argparser())
69
69
 
70
70
 
71
+ def get_argparsers_args_list() -> list:
72
+ '''Returns list of all args that we know about from eda_config, util, eda.
73
+
74
+ All items will include the -- prefix (--help, etc)'''
75
+ return util.get_argparsers_args_list(parsers=[
76
+ eda_config.get_argparser(),
77
+ util.get_argparser(),
78
+ get_argparser()
79
+ ])
80
+
81
+
71
82
  def get_eda_exec(command: str = '') -> str:
72
83
  '''Returns the full path of `eda` executable to be used for a given eda <command>'''
73
84
  # NOTE(drew): This is kind of flaky. 'eda multi' reinvokes 'eda'. But the executable for 'eda'
@@ -184,7 +195,7 @@ class Tool:
184
195
  return
185
196
 
186
197
 
187
- class Command:
198
+ class Command: # pylint: disable=too-many-public-methods
188
199
  '''Base class for all: eda <command>
189
200
 
190
201
  The Command class should be used when you don't require files, otherwise consider
@@ -245,6 +256,8 @@ class Command:
245
256
  self.target_path = ""
246
257
  self.status = 0
247
258
  self.errors_log_f = None
259
+ self.auto_tool_applied = False
260
+ self.tool_changed_respawn = {}
248
261
 
249
262
 
250
263
  def error(self, *args, **kwargs) -> None:
@@ -267,7 +280,7 @@ class Command:
267
280
  typ='text', description='EDA reported errors'
268
281
  )
269
282
 
270
- except FileNotFoundError:
283
+ except Exception:
271
284
  pass
272
285
  if self.errors_log_f:
273
286
  print(
@@ -277,6 +290,18 @@ class Command:
277
290
 
278
291
  self.status = util.error(*args, **kwargs) # error_code passed and returned via kwargs
279
292
 
293
+ def stop_process_tokens_before_do_it(self) -> bool:
294
+ '''Used by derived classes process_tokens() to know an error was reached
295
+ and to not perform the command (avoid calling do_it())
296
+
297
+ Also used to know if a DEPS target requested a --tool=<value> change and that
298
+ we should respawn the job.'''
299
+ util.debug('stop_process_tokens_before_do_it:',
300
+ f'{self.status=} {self.tool_changed_respawn=} {self.args.get("tool", "")=}')
301
+ if self.tool_changed_respawn or self.status_any_error():
302
+ return True
303
+ return False
304
+
280
305
  def status_any_error(self, report=True) -> bool:
281
306
  '''Used by derived classes process_tokens() to know an error was reached
282
307
  and to not perform the command. Necessary for pytests that use eda.main()'''
@@ -288,6 +313,26 @@ class Command:
288
313
  '''Returns a str for the tool name used for the requested command'''
289
314
  return which_tool(command, config=self.config)
290
315
 
316
+ def safe_which_tool(self, command: str = '') -> str:
317
+ '''Returns a str for the tool name used for the requested command,
318
+
319
+ avoids NotImplementedError (for CommandMulti)'''
320
+
321
+ if getattr(self, '_TOOL', ''):
322
+ return self._TOOL
323
+
324
+ if not command:
325
+ command = getattr(self, 'command_name', '')
326
+
327
+ try:
328
+ if getattr(self, 'which_tool', None):
329
+ return self.which_tool(command)
330
+ except NotImplementedError:
331
+ pass
332
+
333
+ return which_tool(command, config=self.config)
334
+
335
+
291
336
  def create_work_dir( # pylint: disable=too-many-branches,too-many-statements
292
337
  self
293
338
  ) -> str:
@@ -361,7 +406,7 @@ class Command:
361
406
  # Do not allow other absolute path work dirs if it already exists.
362
407
  # This prevents you from --work-dir=~ and eda wipes out your home dir.
363
408
  self.error(f'Cannot use work-dir={self.args["work-dir"]} starting with',
364
- 'fabsolute path "/"')
409
+ 'absolute path "/"')
365
410
  elif str(Path('..')) in str(Path(self.args['work-dir'])):
366
411
  # Do not allow other ../ work dirs if it already exists.
367
412
  self.error(f'Cannot use work-dir={self.args["work-dir"]} with up-hierarchy'
@@ -370,9 +415,16 @@ class Command:
370
415
  # If we made it this far, on a directory that exists, that appears safe
371
416
  # to delete and re-create:
372
417
  util.info(f"Removing previous '{self.args['work-dir']}'")
373
- shutil.rmtree(self.args['work-dir'])
374
- util.safe_mkdir(self.args['work-dir'])
375
- util.debug(f'create_work_dir: created {self.args["work-dir"]}')
418
+ try:
419
+ shutil.rmtree(self.args['work-dir'])
420
+ util.safe_mkdir(self.args['work-dir'])
421
+ util.debug(f'create_work_dir: created {self.args["work-dir"]}')
422
+ except PermissionError as e:
423
+ self.error('Could not remove existing dir and create new due to filesystem',
424
+ f'PermissionError: {self.args["work-dir"]}; exception: {e}')
425
+ except Exception as e:
426
+ self.error('Could not remove existing dir and create new due to internal',
427
+ f'Exception: {self.args["work-dir"]}; exception: {e}')
376
428
  else:
377
429
  util.safe_mkdir(self.args['work-dir'])
378
430
  util.debug(f'create_work_dir: created {self.args["work-dir"]}')
@@ -492,12 +544,14 @@ class Command:
492
544
 
493
545
 
494
546
  def get_argparser( # pylint: disable=too-many-branches
495
- self, parser_arg_list=None
547
+ self, parser_arg_list=None, support_underscores: bool = True,
496
548
  ) -> argparse.ArgumentParser:
497
549
  ''' Returns an argparse.ArgumentParser() based on self.args (dict)
498
550
 
499
551
  If parser_arg_list is not None, the ArgumentParser() is created using only the keys in
500
552
  self.args provided by the list parser_arg_list.
553
+
554
+ If support_underscores=False, then only return an ArgumentParser() with --arg-posix-dashes
501
555
  '''
502
556
 
503
557
  # Preference is --args-with-dashes, which then become parsed.args_with_dashes, b/c
@@ -519,9 +573,9 @@ class Command:
519
573
  util.warning(f'{key=} has _ chars, prefer -')
520
574
 
521
575
  keys = [key] # make a list
522
- if '_' in key:
576
+ if support_underscores and '_' in key:
523
577
  keys.append(key.replace('_', '-')) # switch to POSIX dashes for argparse
524
- elif '-' in key:
578
+ elif support_underscores and '-' in key:
525
579
  keys.append(key.replace('-', '_')) # also support --some_arg_with_underscores
526
580
 
527
581
  arguments = [] # list supplied to parser.add_argument(..) so one liner supports both.
@@ -546,7 +600,7 @@ class Command:
546
600
  # be --some-bool or --no-some-bool.
547
601
  parser.add_argument(
548
602
  *arguments, default=None, **bool_action_kwargs, **help_kwargs)
549
- elif isinstance(value, list):
603
+ elif isinstance(value, (list, set)):
550
604
  parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
551
605
  elif isinstance(value, (int, str)):
552
606
  parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
@@ -637,7 +691,8 @@ class Command:
637
691
  '''
638
692
 
639
693
  _, unparsed = self.run_argparser_on_list(tokens)
640
- if process_all and len(unparsed) > 0:
694
+ if process_all and unparsed:
695
+ self.warning_show_known_args()
641
696
  self.error(f"Didn't understand argument: '{unparsed=}' in",
642
697
  f" {self.command_name=} context, {pwd=}",
643
698
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
@@ -718,7 +773,7 @@ class Command:
718
773
 
719
774
 
720
775
  def help( # pylint: disable=dangerous-default-value,too-many-branches
721
- self, tokens: list = []
776
+ self, tokens: list = [], no_targets: bool = False
722
777
  ) -> None:
723
778
  '''Since we don't quite follow standard argparger help()/usage(), we'll format our own
724
779
 
@@ -733,7 +788,10 @@ class Command:
733
788
  # using bare 'print' here, since help was requested, avoids --color and --quiet
734
789
  print()
735
790
  print('Usage:')
736
- print(f' eda [options] {self.command_name} [options] [files|targets, ...]')
791
+ if no_targets:
792
+ print(f' eda [options] {self.command_name} [options]')
793
+ else:
794
+ print(f' eda [options] {self.command_name} [options] [files|targets, ...]')
737
795
  print()
738
796
 
739
797
  print_base_help()
@@ -780,6 +838,53 @@ class Command:
780
838
  if unparsed:
781
839
  print(f'Unparsed args: {unparsed}')
782
840
 
841
+ def get_argparsers_args_list(self) -> list:
842
+ '''Returns list of all args that we know about from eda_config, util, eda, and our self.args
843
+
844
+ All items will include the -- prefix (--help, etc)'''
845
+ return util.get_argparsers_args_list(parsers=[
846
+ eda_config.get_argparser(),
847
+ util.get_argparser(),
848
+ get_argparser(),
849
+ self.get_argparser(support_underscores=False)
850
+ ])
851
+
852
+ def pretty_str_known_args(self, command: str = '') -> str:
853
+ '''Returns multiple line column organized string of all known args'''
854
+ _command = command
855
+ if not _command:
856
+ _command = self.command_name
857
+
858
+ _args_list = self.get_argparsers_args_list()
859
+ _pretty_args_list = pretty_list_columns_manual(data=_args_list)
860
+ return (f"Known args for command '{_command}' :\n"
861
+ " " + "\n ".join(_pretty_args_list)
862
+ )
863
+
864
+ def warning_show_known_args(self, command: str = '') -> None:
865
+ '''Print a helpful warning showing available args for this eda command (or commands)'''
866
+
867
+ if not command:
868
+ commands = [self.command_name]
869
+ else:
870
+ commands = command.split() # support for command="multi sim"
871
+
872
+ _tool = self.safe_which_tool(commands[0]) # use first command if > 1 presented
873
+ lines = []
874
+ if _tool:
875
+ lines.append(f"To see all args for command(s) {commands}, tool '{_tool}', run:")
876
+ else:
877
+ lines.append(f"To see all args for command(s) {commands}, run:")
878
+
879
+ for cmd in commands:
880
+ if _tool:
881
+ lines.append(f" eda {cmd} --tool={_tool} --help")
882
+ else:
883
+ lines.append(f" eda {cmd} --help")
884
+
885
+ lines.append(self.pretty_str_known_args(command=commands[-1])) # use last command if > 1
886
+ util.warning("\n".join(lines))
887
+
783
888
 
784
889
  class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
785
890
  '''CommandDesign is the eda base class for command handlers that need to track files.
@@ -1345,10 +1450,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1345
1450
  plusarg = strip_outer_quotes(token)
1346
1451
  self.process_plusarg(plusarg, pwd=pwd)
1347
1452
  remove_list.append(token)
1453
+
1348
1454
  for x in remove_list:
1349
1455
  unparsed.remove(x)
1350
1456
 
1351
- if not unparsed and self.error_on_no_files_or_targets:
1457
+ if self.error_on_no_files_or_targets and not unparsed:
1352
1458
  # derived classes can set error_on_no_files_or_targets=True
1353
1459
  # For example: CommandSim will error (requires files/targets),
1354
1460
  # CommandWaves does not (files/targets not required)
@@ -1367,13 +1473,27 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1367
1473
  util.warning(f"For command '{self.command_name}' no files or targets were",
1368
1474
  f"presented at the command line, so using '{target}' from",
1369
1475
  f"{deps.deps_file}")
1370
- if len(unparsed) == 0:
1476
+ if not unparsed:
1371
1477
  # If unparsed is still empty, then error.
1372
1478
  self.error(f"For command '{self.command_name}' no files or targets were",
1373
1479
  f"presented at the command line: {orig_tokens}",
1374
1480
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1375
1481
 
1376
1482
  # by this point hopefully this is a target ... is it a simple filename?
1483
+
1484
+ # Before we look for files, check for stray --some-arg in unparsed, we don't want to treat
1485
+ # these as potential targets if process_all=True, but someone might have a file named
1486
+ # --my_file.sv, so those are technically allowed until the tool would fail on them.
1487
+ possible_unparsed_args = [
1488
+ x for x in unparsed if x.startswith('--') and not os.path.isfile(x)
1489
+ ]
1490
+ if process_all and possible_unparsed_args:
1491
+ _tool = self.safe_which_tool()
1492
+ self.warning_show_known_args()
1493
+ self.error(f"Didn't understand unparsed args: {possible_unparsed_args}, for command",
1494
+ f"'{self.command_name}', tool '{_tool}'",
1495
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1496
+
1377
1497
  remove_list = []
1378
1498
  last_potential_top_file = ('', '') # (top, fpath)
1379
1499
  last_potential_top_target = ('', '') # (top, path/to/full-target-name)
@@ -1405,6 +1525,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1405
1525
 
1406
1526
  # we appear to be dealing with a target name which needs to be resolved (usually
1407
1527
  # recursively)
1528
+ if token.startswith('-'):
1529
+ # We are not going to handle targets that start with a -, it's likely
1530
+ # an unparsed arg.
1531
+ continue
1408
1532
  if token.startswith(os.sep):
1409
1533
  target_name = token # if it's absolute path, don't prepend anything
1410
1534
  else:
@@ -1424,8 +1548,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1424
1548
  unparsed.remove(x)
1425
1549
 
1426
1550
  # we were unable to figure out what this command line token is for...
1427
- if process_all and len(unparsed) > 0:
1428
- self.error(f"Didn't understand command remaining tokens {unparsed=} in CommandDesign",
1551
+ if process_all and unparsed:
1552
+ self.warning_show_known_args()
1553
+ self.error(f"Didn't understand remaining args or targets {unparsed=} for command",
1554
+ f"'{self.command_name}'",
1429
1555
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1430
1556
 
1431
1557
  # handle a missing self.args['top'] with last filepath or last target:
@@ -1462,6 +1588,14 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1462
1588
  f"'{self.command_name}' for tool={self.args.get('tool', None)}",
1463
1589
  error_code=status_constants.EDA_COMMAND_MISSING_TOP)
1464
1590
 
1591
+ if self.tool_changed_respawn:
1592
+ util.warning(
1593
+ 'CommandDesign: need to respawn due to tool change to',
1594
+ f'\'{self.tool_changed_respawn["tool"]}\' from',
1595
+ f'\'{self.tool_changed_respawn["orig_tool"]}\'',
1596
+ f'(from DEPS, {self.tool_changed_respawn["from"]})'
1597
+ )
1598
+
1465
1599
  return unparsed
1466
1600
 
1467
1601
 
@@ -1961,6 +2095,7 @@ class CommandParallel(Command):
1961
2095
  # There should not be any single_cmd_unparsed args starting with '-'
1962
2096
  bad_remaining_args = [x for x in single_cmd_unparsed if x.startswith('-')]
1963
2097
  if bad_remaining_args:
2098
+ self.warning_show_known_args(command=f'{self.command_name} {command}')
1964
2099
  self.error(f'for {self.command_name} {command=} the following args are unknown',
1965
2100
  f'{bad_remaining_args}',
1966
2101
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
@@ -2020,8 +2155,12 @@ class CommandParallel(Command):
2020
2155
  tpath, _ = os.path.split(job_dict['target'])
2021
2156
 
2022
2157
  # prepend path information to job-name:
2023
- patched_target_path = os.path.relpath(tpath).replace(os.sep, '_')
2024
- new_job_name = f'{patched_target_path}.{key}'
2158
+ patched_target_path = os.path.relpath(tpath).replace(os.sep, '_').lstrip('.')
2159
+ if patched_target_path:
2160
+ new_job_name = f'{patched_target_path}.{key}'
2161
+ else:
2162
+ continue # there's nothing to "patch", our job-name will be unchanged.
2163
+
2025
2164
  replace_job_arg(job_dict, arg_name='job-name', new_value=new_job_name)
2026
2165
 
2027
2166
  # prepend path information to force-logfile (if present):
opencos/eda_config.py CHANGED
@@ -28,7 +28,7 @@ class Defaults:
28
28
  config_yml = ''
29
29
 
30
30
  supported_config_keys = set([
31
- 'DEFAULT_HANDLERS',
31
+ 'DEFAULT_HANDLERS', 'DEFAULT_HANDLERS_HELP',
32
32
  'defines',
33
33
  'dep_command_enables',
34
34
  'dep_tags_enables',