opencos-eda 0.3.11__py3-none-any.whl → 0.3.13__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/flist.py +222 -39
  2. opencos/commands/multi.py +5 -3
  3. opencos/commands/sim.py +121 -7
  4. opencos/commands/sweep.py +1 -1
  5. opencos/commands/upload.py +4 -4
  6. opencos/commands/waves.py +44 -0
  7. opencos/deps/deps_file.py +4 -1
  8. opencos/deps_schema.py +2 -2
  9. opencos/eda.py +284 -175
  10. opencos/eda_base.py +54 -22
  11. opencos/eda_config.py +51 -24
  12. opencos/eda_config_defaults.yml +326 -195
  13. opencos/eda_config_reduced.yml +19 -39
  14. opencos/eda_tool_helper.py +193 -24
  15. opencos/tools/cocotb.py +6 -29
  16. opencos/tools/invio.py +0 -6
  17. opencos/tools/iverilog.py +16 -16
  18. opencos/tools/modelsim_ase.py +1 -13
  19. opencos/tools/quartus.py +81 -24
  20. opencos/tools/questa.py +0 -14
  21. opencos/tools/questa_common.py +95 -30
  22. opencos/tools/questa_fe.py +0 -14
  23. opencos/tools/questa_fse.py +0 -14
  24. opencos/tools/riviera.py +56 -25
  25. opencos/tools/slang.py +15 -12
  26. opencos/tools/slang_yosys.py +0 -6
  27. opencos/tools/surelog.py +14 -11
  28. opencos/tools/tabbycad_yosys.py +1 -7
  29. opencos/tools/verilator.py +18 -14
  30. opencos/tools/vivado.py +57 -22
  31. opencos/tools/yosys.py +2 -4
  32. opencos/util.py +14 -5
  33. opencos/utils/str_helpers.py +42 -1
  34. {opencos_eda-0.3.11.dist-info → opencos_eda-0.3.13.dist-info}/METADATA +1 -2
  35. {opencos_eda-0.3.11.dist-info → opencos_eda-0.3.13.dist-info}/RECORD +40 -41
  36. opencos/eda_config_max_verilator_waivers.yml +0 -39
  37. {opencos_eda-0.3.11.dist-info → opencos_eda-0.3.13.dist-info}/WHEEL +0 -0
  38. {opencos_eda-0.3.11.dist-info → opencos_eda-0.3.13.dist-info}/entry_points.txt +0 -0
  39. {opencos_eda-0.3.11.dist-info → opencos_eda-0.3.13.dist-info}/licenses/LICENSE +0 -0
  40. {opencos_eda-0.3.11.dist-info → opencos_eda-0.3.13.dist-info}/licenses/LICENSE.spdx +0 -0
  41. {opencos_eda-0.3.11.dist-info → opencos_eda-0.3.13.dist-info}/top_level.txt +0 -0
opencos/eda.py CHANGED
@@ -20,6 +20,7 @@ from pathlib import Path
20
20
  import opencos
21
21
  from opencos import util, eda_config, eda_base
22
22
  from opencos.eda_base import Command, Tool, which_tool, print_eda_usage_line
23
+ from opencos.eda_tool_helper import pretty_info_handler_tools
23
24
  from opencos.files import safe_shutil_which
24
25
  from opencos.util import safe_emoji, Colors
25
26
  from opencos.utils import vsim_helper, vscode_helper
@@ -43,18 +44,30 @@ util.global_log.default_log_disable_with_args.extend([
43
44
  # These are also overriden depending on the tool, for example --tool verilator sets
44
45
  # "sim": CommandSimVerilator.
45
46
  def init_config(
46
- config: dict, quiet: bool = False, tool=None, command: str = '',
47
+ config: dict, quiet: bool = False, tool=None, command: str = '', sub_command: str = '',
47
48
  run_auto_tool_setup: bool = True
48
49
  ) -> dict:
49
- '''Sets or clears entries in config (dict) so tools can be re-loaded.'''
50
+ '''Sets or clears entries in config (dict) so tools can be re-loaded.
50
51
 
51
- # For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
52
- # the actual class using importlib (via opencos.util)
52
+ - For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
53
+ the actual class using importlib (via opencos.util)
54
+ - 'command', 'sub_command', and 'run_auto_tool_setup' are passed by the config dict,
55
+ the args exist are more for test hooks and debug.
56
+ '''
53
57
 
54
58
  eda_config.update_config_auto_tool_order_for_tool(tool=tool, config=config)
55
59
 
60
+ if not command and 'command' in config:
61
+ command = config.get('command', '')
62
+ if not sub_command and 'sub_command' in config:
63
+ sub_command = config.get('sub_command', '')
64
+ if 'run_auto_tool_setup' in config:
65
+ run_auto_tool_setup = config.get('run_auto_tool_setup', True)
66
+
56
67
  config['command_handler'] = {}
57
68
  for _cmd, str_class in config['DEFAULT_HANDLERS'].items():
69
+ if (command or sub_command) and _cmd not in (command, sub_command):
70
+ continue # don't bother importing a default handler class if we won't use it.
58
71
  cls = util.import_class_from_string(str_class)
59
72
  if not cls:
60
73
  util.error(f"config DEFAULT_HANDLERS command={_cmd} {str_class=} could not import")
@@ -62,9 +75,24 @@ def init_config(
62
75
  config['command_handler'][_cmd] = cls
63
76
 
64
77
  config['auto_tools_found'] = {}
65
- config['tools_loaded'] = set()
78
+ config['tools_loaded'] = []
79
+
80
+ # If this is a sub_command sitation, then use that for setting up tools, otherwise
81
+ # use the command. If there's no command, blank str is fine will load all the tools, or
82
+ # just the specified 'tool'.
83
+ _command_for_tool_setup = command
84
+ if sub_command:
85
+ _command_for_tool_setup = sub_command
86
+
66
87
  if run_auto_tool_setup:
67
- config = auto_tool_setup(config=config, quiet=quiet, tool=tool, command=command)
88
+ # Run this with quiet=True, we will print tool/version/path later.
89
+ config = auto_tool_setup(
90
+ config=config, quiet=True, tool=tool, command=_command_for_tool_setup
91
+ )
92
+
93
+ if not quiet:
94
+ pretty_info_handler_tools(config=config, command=_command_for_tool_setup)
95
+
68
96
  return config
69
97
 
70
98
 
@@ -117,11 +145,11 @@ def usage(tokens: list, config: dict, command: str = "", tool: str = "") -> int:
117
145
  return 0
118
146
 
119
147
  if command in config['command_handler'].keys():
120
- sco = config['command_handler'][command](config=config) # sub command object
121
- sco_tool = getattr(sco, '_TOOL', '')
122
- if tool and tool != sco_tool:
148
+ cmd_obj = config['command_handler'][command](config=config) # command object
149
+ cmd_obj_tool = getattr(cmd_obj, '_TOOL', '')
150
+ if tool and tool != cmd_obj_tool:
123
151
  util.warning(f'{tool=} does not support {command=}')
124
- sco.help(tokens=tokens)
152
+ cmd_obj.help(tokens=tokens)
125
153
  return util.exit(0)
126
154
 
127
155
  util.info("Valid commands are:")
@@ -158,59 +186,75 @@ def interactive(config: dict) -> int:
158
186
 
159
187
 
160
188
  def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
161
- warnings: bool = True, config = None, quiet: bool = False, tool: str = '',
189
+ config = None, quiet: bool = False, tool: str = '',
162
190
  command: str = ''
163
191
  ) -> dict:
164
- '''Returns an updated config, uses config['auto_tools_order'][0] dict, calls tool_setup(..)
192
+ '''Returns an updated config, uses config['auto_tools_order'] dict and
193
+ config['tools'] dict, calls tool_setup(..)
165
194
 
166
- -- adds items to config['tools_loaded'] set
195
+ -- adds items to config['tools_loaded'] list
167
196
  -- updates config['command_handler'][command] with a Tool class
168
197
 
169
198
  Input arg tool can be in the form (for example):
170
199
  tool='verlator', tool='verilator=/path/to/verilator.exe'
171
- If so, updates config['auto_tools_order'][tool]['exe']
200
+ If so, updates config['tools'][tool]['exe']
172
201
  '''
173
202
 
174
203
  tool = eda_config.tool_arg_remove_path_information(tool)
175
204
 
176
205
  assert 'auto_tools_order' in config
177
- assert isinstance(config['auto_tools_order'], list)
178
- assert isinstance(config['auto_tools_order'][0], dict)
206
+ assert isinstance(config['auto_tools_order'], dict)
207
+ assert 'tools' in config
208
+ assert isinstance(config['tools'], dict)
209
+
210
+ if 'auto_tools_found' not in config:
211
+ config['auto_tools_found'] = {}
212
+ if 'tools_loaded' not in config:
213
+ config['tools_loaded'] = []
214
+
215
+ util.debug(f'Calling auto_tool_setup for {tool=} {command=}')
179
216
 
180
217
  if command:
181
218
  util.info(f'Auto tool setup for command: {Colors.byellow}{command}')
182
219
 
183
- for name, value in config['auto_tools_order'][0].items():
184
- if tool and tool != name:
185
- # if called with tool=(some_name), then only load that tool (which is not
186
- # this one)
220
+ # We'd like to achieve two things:
221
+ # 1. Load tool(s) based on args tool, command, config (and what's in config['tools']
222
+ # 2. Select the handling class for a command based on config['auto_tools_order']
223
+
224
+ # Step 1 - Load tools (do not set config['command_handler'][command])
225
+
226
+ if tool:
227
+ # if called with tool=(some_name), then only load that tool (which is not
228
+ # this one). Also delete this entry from config['tools'] because we don't need it.
229
+ for name in list(config['tools'].keys()):
230
+ if tool != name:
231
+ del config['tools'][name]
232
+
233
+ for name, tool_cfg in config['tools'].items():
234
+
235
+ if name in config['auto_tools_found']:
236
+ # we already loaded this tool.
187
237
  continue
188
238
 
189
- if command and command not in value.get('handlers', {}) and \
239
+ handlers = config['tools'].get(name, {}).get('handlers', {})
240
+ if command and command not in handlers and \
190
241
  command not in config.get('command_has_subcommands', []):
191
- # Skip tool_setup(..) if the tool handlers can't support command,
192
- # this is a time-saving feature, but if the comman is multi, tools-multi,
193
- # sweep, then don't skip this (we don't know what tools we need so load them
194
- # all.
195
- # We could figure this out if we went looking for all command(s)
196
- # multi + sub-command, but that's slightly dangerous if we grab a 'command'
197
- # from another arg.
198
242
  util.debug(f"Skipping tool {name} because it cannot handle {command=}")
199
243
  continue
200
244
 
201
245
  util.debug(f"Checking for ability to run tool: {name}")
202
- exe = value.get('exe', str())
246
+
247
+ exe = tool_cfg.get('exe', str())
203
248
  if isinstance(exe, list):
204
249
  exe_list = exe
205
250
  elif isinstance(exe, str):
206
251
  exe_list = [exe] # make it a list
207
252
  else:
208
- util.error(f'eda.py: config["auto_tools_order"][0] for {name=} {value=} has bad type'
209
- f'for {exe=}')
253
+ util.error(f'eda.py: config["tools"] for {name=} has bad type for {exe=}')
210
254
  continue
211
255
 
212
256
  has_all_py = True
213
- requires_py_list = value.get('requires_py', [])
257
+ requires_py_list = tool_cfg.get('requires_py', [])
214
258
  for pkg in requires_py_list:
215
259
  spec = importlib.util.find_spec(pkg)
216
260
  if not spec:
@@ -218,7 +262,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
218
262
  util.debug(f"... No, missing pkg {spec}")
219
263
 
220
264
  has_all_env = True
221
- requires_env_list = value.get('requires_env', [])
265
+ requires_env_list = tool_cfg.get('requires_env', [])
222
266
  for env in requires_env_list:
223
267
  if not os.environ.get(env, ''):
224
268
  has_all_env = False
@@ -228,20 +272,20 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
228
272
  has_all_in_exe_path = True
229
273
  exe_path = None
230
274
  for exe in exe_list:
231
- assert exe != '', f'{name=} {value=} value missing "exe" {exe=}'
275
+ assert exe != '', f'{name=} value missing "exe" {exe=}'
232
276
  p = safe_shutil_which(exe)
233
277
  if not exe_path:
234
278
  exe_path = p # set on first required exe
235
279
  if not p:
236
280
  has_all_exe = False
237
281
  util.debug(f"... No, missing exe {exe}")
238
- for req in value.get('requires_in_exe_path', []):
282
+ for req in tool_cfg.get('requires_in_exe_path', []):
239
283
  if p and req and str(Path(req)) not in str(Path(p)):
240
284
  has_all_in_exe_path = False
241
285
  util.debug(f"... No, missing path requirement {req}")
242
286
 
243
287
  has_vsim_helper = True
244
- if value.get('requires_vsim_helper', False):
288
+ if tool_cfg.get('requires_vsim_helper', False):
245
289
  # This tool name must be in opencos.utils.vsim_helper.TOOL_PATH[name].
246
290
  # Special case for vsim being used by a lot of tools.
247
291
  vsim_helper.init() # only runs checks once internally
@@ -249,7 +293,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
249
293
  has_vsim_helper = bool(exe_path)
250
294
 
251
295
  has_vscode_helper = True
252
- needs_vscode_extensions = value.get('requires_vscode_extension', None)
296
+ needs_vscode_extensions = tool_cfg.get('requires_vscode_extension', None)
253
297
  if needs_vscode_extensions:
254
298
  if not isinstance(needs_vscode_extensions, list):
255
299
  util.error(
@@ -262,16 +306,15 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
262
306
  )
263
307
 
264
308
  if has_all_exe:
265
- requires_cmd_list = value.get('requires_cmd', [])
309
+ requires_cmd_list = tool_cfg.get('requires_cmd', [])
266
310
  for cmd in requires_cmd_list:
267
311
  cmd_list = shlex.split(cmd)
268
312
  try:
269
313
  proc = subprocess.run(cmd_list, capture_output=True, check=False,
270
314
  input=b'exit\n\n')
271
315
  if proc.returncode != 0:
272
- if not quiet:
273
- util.debug(f"For tool {name} missing required command",
274
- f"({proc.returncode=}): {cmd_list=}")
316
+ util.debug(f"For tool {name} missing required command",
317
+ f"({proc.returncode=}): {cmd_list=}")
275
318
  has_all_exe = False
276
319
  except Exception as e:
277
320
  has_all_exe = False
@@ -285,97 +328,128 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
285
328
  else:
286
329
  p = safe_shutil_which(exe_list[0])
287
330
  config['auto_tools_found'][name] = p # populate key-value pairs w/ first exe in list
331
+ if name not in config['tools_loaded']:
332
+ config['tools_loaded'].append(name)
288
333
  if not quiet:
289
334
  util.info(f"Detected {name} ({p})")
290
- tool_setup(
291
- tool=name, quiet=True, auto_setup=(not tool), warnings=warnings, config=config
292
- )
293
335
  else:
294
336
  util.debug(f'Tool {name} is missing one of: {has_all_py=} {has_all_env=}',
295
337
  f'{has_all_exe=} {has_all_in_exe_path=} {has_vsim_helper=}',
296
338
  f'{has_vscode_helper=}')
297
339
 
340
+ # Step 2 - set command handlers for command/tool pairs.
341
+
342
+ # If --tool was specified (this function's arg, tool) then we don't need to check
343
+ # config['auto_tools_order'], simply load the handlers this tool supports.
344
+ if tool:
345
+ handlers = config['tools'].get(tool, {}).get('handlers', {})
346
+ if command and command in handlers:
347
+ # --tool and command were set, only load that one handler:
348
+ set_command_handler(
349
+ command=command, tool=tool, config=config, auto_setup=False,
350
+ )
351
+ return config
352
+
353
+ for _command, _ in handlers.items():
354
+ # set all handlers, command was not set or was not in handlers.
355
+ set_command_handler(
356
+ command=_command, tool=tool, config=config, auto_setup=False,
357
+ )
358
+ return config
359
+
360
+ # If --tool was not set, look at config['auto_tools_found'] should be populated based on
361
+ # config['tools'], and if we called this method (auto_tool_setup(...)) with tool= and command=
362
+ # arg(s). Call set_command_handlers for each command/tool we need to:
363
+ for _command, list_of_tools in config['auto_tools_order'].items():
364
+
365
+ for _tool in list_of_tools:
366
+ if _tool not in config['tools_loaded']:
367
+ continue
368
+
369
+ set_command_handler(
370
+ command=_command, tool=_tool, config=config, auto_setup=(not tool),
371
+ )
372
+
373
+
298
374
  return config
299
375
 
300
376
 
301
- def tool_setup( # pylint: disable=too-many-branches
302
- tool: str, config: dict, quiet: bool = False, auto_setup: bool = False,
303
- warnings: bool = True
377
+ def set_command_handler( # pylint: disable=too-many-branches
378
+ command: str, tool: str, config: dict, auto_setup: bool = False
304
379
  ):
305
- ''' Adds items to config["tools_loaded"] (set) and updates config['command_handler'].
306
-
307
- config is potentially updated for entry ['command_handler'][command] with a Tool class.
380
+ '''Updates config['command_handler'] with Command/Tool class
308
381
 
309
382
  Input arg tool can be in the form (for example):
310
383
  tool='verlator', tool='verilator=/path/to/verilator.exe'
311
384
 
312
385
  '''
313
386
 
314
- tool = eda_config.tool_arg_remove_path_information(tool)
315
-
316
- if not quiet and not auto_setup:
317
- util.info(f"Setup for tool: '{tool}'")
318
387
 
388
+ tool = eda_config.tool_arg_remove_path_information(tool)
319
389
  if not tool:
320
390
  return
321
391
 
322
- if tool not in config['auto_tools_order'][0]:
323
- tools = list(config.get('auto_tools_order', [{}])[0].keys())
324
- cfg_yaml_fname = config.get('config-yml', None)
325
- util.warning(f'Unknown tool: {tool}')
326
- if tools:
327
- util.info('Known tools:')
328
- pretty_tools = str_helpers.pretty_list_columns_manual(data=tools)
329
- for row in pretty_tools:
330
- if row:
331
- util.info(row)
332
- util.error(f"Don't know how to run tool_setup({tool=}), is not in",
333
- f"config['auto_tools_order'] from {cfg_yaml_fname}")
392
+ entry = config['tools'][tool]
393
+ tool_cmd_handler_dict = entry.get('handlers', {})
394
+
395
+ current_handler_cls = config['command_handler'].get(command, None)
396
+
397
+ if auto_setup and current_handler_cls is not None and issubclass(current_handler_cls, Tool):
398
+ # If we're not in auto_setup, then always override (aka arg --tool=<this tool>)
399
+ # skip, already has a tool associated with it, and we're in auto_setup=True
334
400
  return
335
401
 
336
- if tool not in config['auto_tools_found']:
337
- cfg_yaml_fname = config.get('config-yml', None)
338
- util.error(f"Don't know how to run tool_setup({tool=}), is not in",
339
- f"{config['auto_tools_found']=} from {cfg_yaml_fname}")
402
+ str_class_name = tool_cmd_handler_dict.get(command, '')
403
+ if not str_class_name:
404
+ # This tool doesn't have a handler for this command
340
405
  return
341
406
 
342
- if auto_setup and tool is not None and tool in config['tools_loaded']:
343
- # Do I realy need to warn if a tool was loaded from auto_tool_setup(),
344
- # but then I also called it via --tool verilator? Only warn if auto_setup=True:
345
- if warnings:
346
- util.warning(f"tool_setup: {auto_setup=} already setup for {tool}?")
407
+ cls = util.import_class_from_string(str_class_name)
347
408
 
348
- entry = config['auto_tools_order'][0].get(tool, {})
349
- tool_cmd_handler_dict = entry.get('handlers', {})
409
+ if command in config.get('command_determines_tool', []) + \
410
+ config.get('command_tool_is_optional', []):
411
+ # we don't need to confirm the handler parent is a Tool class.
412
+ pass
413
+ else:
414
+ assert issubclass(cls, Tool), \
415
+ f'{str_class_name=} is does not have Tool class associated with it'
350
416
 
351
- for command, str_class_name in tool_cmd_handler_dict.items():
352
- current_handler_cls = config['command_handler'].get(command, None)
417
+ if not auto_setup or \
418
+ command not in config.get('command_determines_tool', []):
419
+ # If not auto_setup - then someone called this --tool by name on the command line,
420
+ # then update the command handler
421
+ # otherwise, if --tool was not set, and command determines tool, leave it with
422
+ # the default handler.
423
+ util.debug(f'Setting {cls=} for {command=} in config.command_handler')
424
+ config['command_handler'][command] = cls
353
425
 
354
- if auto_setup and current_handler_cls is not None and issubclass(current_handler_cls, Tool):
355
- # If we're not in auto_setup, then always override (aka arg --tool=<this tool>)
356
- # skip, already has a tool associated with it, and we're in auto_setup=True
357
- continue
358
426
 
359
- cls = util.import_class_from_string(str_class_name)
360
427
 
361
- if command in config.get('command_determines_tool', []) + \
362
- config.get('command_tool_is_optional', []):
363
- # we don't need to confirm the handler parent is a Tool class.
364
- pass
365
- else:
366
- assert issubclass(cls, Tool), \
367
- f'{str_class_name=} is does not have Tool class associated with it'
428
+ def get_and_remove_command_from_tokens(
429
+ tokens: list, config: dict, is_sub_command: bool = False
430
+ ) -> str:
431
+ '''Attempts to get and remove an eda command (sim, flist, multi, etc) from list
432
+
433
+ - if is_sub_command=True and a sub command was found and not alread set, then add it
434
+ to config['sub_command']
435
+ - if is_sub_command=False, and a command is not already set, then add the found command
436
+ to config['command']
437
+ '''
368
438
 
369
- if not auto_setup or \
370
- command not in config.get('command_determines_tool', []):
371
- # If not auto_setup - then someone called this --tool by name on the command line,
372
- # then update the command handler
373
- # otherwise, if --tool was not set, and command determines tool, leave it with
374
- # the default handler.
375
- util.debug(f'Setting {cls=} for {command=} in config.command_handler')
376
- config['command_handler'][command] = cls
439
+ command = ''
440
+ for value in tokens:
441
+ if value in config['DEFAULT_HANDLERS'].keys():
442
+ if is_sub_command and value in config['command_has_subcommands']:
443
+ continue # only 1 sub command allowed, not a chain.
444
+ command = value
445
+ tokens.remove(value) # remove command (flist, export, targets, etc)
446
+ if not is_sub_command and not config.get('command', ''):
447
+ config['command'] = value
448
+ if is_sub_command and not config.get('sub_command', ''):
449
+ config['sub_command'] = value
450
+ break
377
451
 
378
- config['tools_loaded'].add(tool)
452
+ return command
379
453
 
380
454
 
381
455
  def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-return-statements
@@ -395,7 +469,7 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
395
469
 
396
470
  deferred_tokens = []
397
471
  command = ""
398
- run_auto_tool_setup = True
472
+ sub_command = ""
399
473
 
400
474
  parser = eda_base.get_argparser()
401
475
  try:
@@ -420,28 +494,31 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
420
494
 
421
495
  util.debug(f'eda process_tokens: {parsed=} {unparsed=}')
422
496
 
423
- # Attempt to get the 'command' in the unparsed args before we've even
424
- # set the command handlers (some commands don't use tools).
425
- # Note that we only grab the first command, and for multi, tools-multi,
426
- # or sweep we do NOT get the subcommand.
427
- for value in unparsed:
428
- if value in config['DEFAULT_HANDLERS'].keys():
429
- command = value
430
- if not parsed.tool and value in config['command_tool_is_optional']:
431
- # only do this if --tool was not set.
432
- run_auto_tool_setup = False
433
- unparsed.remove(value) # remove command (flist, export, targets, etc)
434
- break
497
+ # Attempt to get the 'command' and optional sub-command. It is removed
498
+ # from unparsed if found, and if a sub_command is found it is set in
499
+ # config['sub_command']
500
+ command = get_and_remove_command_from_tokens(
501
+ tokens=unparsed, config=config, is_sub_command=False
502
+ )
503
+ if command and command in config.get('command_has_subcommands', []):
504
+ sub_command = get_and_remove_command_from_tokens(
505
+ tokens=unparsed, config=config, is_sub_command=True
506
+ )
507
+
508
+ if not parsed.tool:
509
+ if not sub_command and command and command in config['command_tool_is_optional']:
510
+ # --tool not set, and tool was optional for primary command
511
+ config['run_auto_tool_setup'] = False
512
+ if sub_command and sub_command in config['command_tool_is_optional']:
513
+ # --tool not set, and tool was optional for secondary command
514
+ config['run_auto_tool_setup'] = False
435
515
 
436
516
  if not is_interactive:
437
517
  # Run init_config() now, we deferred it in main(), but only run it
438
518
  # for this tool (or tool=None to figure it out)
439
519
  # This will handle any --tool=<name>=/path/to/bin also, so don't have to
440
520
  # run tool_setup(..) on its own.
441
- config = init_config(
442
- config, tool=parsed.tool, command=command,
443
- run_auto_tool_setup=run_auto_tool_setup
444
- )
521
+ config = init_config(config, tool=parsed.tool)
445
522
  if not config:
446
523
  util.error(f'eda.py main: problem loading config, {tokens=}')
447
524
  return 3
@@ -463,69 +540,81 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
463
540
  "valid commands)!")
464
541
  return 2
465
542
 
466
- sco = config['command_handler'][command](config=config) # sub command object
543
+ cmd_obj = config['command_handler'][command](config=config) # command object
467
544
  if not parsed.tool:
468
545
  # then what was the auto selected tool?
469
- sco_tool = getattr(sco, '_TOOL', '')
470
- if sco_tool in config['auto_tools_order'][0] and \
471
- config['auto_tools_order'][0][sco_tool].get('disable-auto', False):
472
- util.error(f'Cannot use tool={sco_tool} without arg --tool, it cannot be selected',
546
+ cmd_obj_tool = getattr(cmd_obj, '_TOOL', '')
547
+ if cmd_obj_tool in config['auto_tools_found'] and \
548
+ config['tools'].get(cmd_obj_tool, {}).get('disable-auto', False):
549
+ util.error(f'Cannot use tool={cmd_obj_tool} without arg --tool, it cannot be selected',
473
550
  'automatically')
474
551
  return status_constants.EDA_COMMAND_OR_ARGS_ERROR
475
552
 
476
- util.debug(f'{command=}')
477
- util.debug(f'{sco.config=}')
478
- util.debug(f'{type(sco)=}')
553
+ util.debug(f'{command=} {sub_command=}')
554
+ util.debug(f'{cmd_obj.config=}')
555
+ util.debug(f'{type(cmd_obj)=}')
479
556
  if not parsed.tool and \
480
557
  command not in config.get('command_determines_tool', []) and \
481
558
  command not in config.get('command_tool_is_optional', []):
559
+ # --tool not set, tool was not optional, so we have to use the tool from the handler cmd_obj
560
+ # (our best guess at tool to use)
482
561
  use_tool = which_tool(command, config)
483
562
  if use_tool:
484
- util.info(f"--tool not specified, using default for {command=}: {use_tool}")
563
+ util.info(f"--tool not specified, using default for {command=}:",
564
+ f"{Colors.bgreen}{use_tool}")
485
565
  else:
486
566
  # Not all commands have a hard requirement on tool (such as 'multi') because we
487
567
  # haven't examined sub-commands yet.
488
- util.info(f'--tool not specified, will attempt to determine tool(s) for {command=}.')
489
- setattr(sco, 'auto_tool_applied', True)
490
-
491
- rc = check_command_handler_cls(command_obj=sco, command=command, parsed_args=parsed)
568
+ util.info('--tool not specified, will attempt to determine tool(s) for',
569
+ f'{Colors.bgreen}{command=}.')
570
+ setattr(cmd_obj, 'auto_tool_applied', True)
571
+
572
+ # For command (and optional sub_command) make sure our handler class is checked to run,
573
+ # (does it require a Tool class, etc)
574
+ rc = check_command_handler_cls(command_obj=cmd_obj, command=command, parsed_args=parsed)
575
+ if sub_command:
576
+ sub_cmd_obj = config['command_handler'][sub_command](config=config) # command object
577
+ rc |= check_command_handler_cls(
578
+ command_obj=sub_cmd_obj, command=sub_command, parsed_args=parsed
579
+ )
492
580
  if rc > 0:
493
- util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=},'
581
+ util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(cmd_obj)=},'
494
582
  f'unparsed={deferred_tokens}')
495
583
  return rc
496
584
 
497
585
  # Add the original, nothing-parsed args to the Command.config dict.
498
- sco.config['eda_original_args'] = original_args
586
+ cmd_obj.config['eda_original_args'] = original_args
499
587
 
500
- setattr(sco, 'command_name', command) # as a safeguard, 'command' is not always passed to 'sco'
501
- unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
588
+ # as a safeguard, the command_name is not always passed to 'cmd_obj', so let's do that now.
589
+ setattr(cmd_obj, 'command_name', command)
590
+ unparsed = cmd_obj.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
502
591
 
503
592
  # query the status from the Command object (0 is pass, > 0 is fail, but we'd prefer to avoid
504
593
  # rc=1 because that's the python exception rc)
505
- rc = getattr(sco, 'status', 2)
506
- util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
594
+ rc = getattr(cmd_obj, 'status', 2)
595
+ util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(cmd_obj)=}, {unparsed=}')
507
596
 
508
- if rc == 0 and not parsed.tool and getattr(sco, 'tool_changed_respawn', False):
509
- return respawn_new_sub_command_object(
510
- sco=sco, parsed=parsed, config=config, command=command, tokens=tokens,
597
+ if rc == 0 and not parsed.tool and getattr(cmd_obj, 'tool_changed_respawn', False):
598
+ return respawn_new_command_object(
599
+ cmd_obj=cmd_obj, parsed=parsed, config=config, command=command, tokens=tokens,
511
600
  deferred_tokens=deferred_tokens
512
601
  )
513
602
 
514
603
  return rc
515
604
 
516
605
 
517
- def respawn_new_sub_command_object(
518
- sco: Command, parsed: argparse.Namespace, config: dict, command: str,
606
+ def respawn_new_command_object(
607
+ cmd_obj: Command, parsed: argparse.Namespace, config: dict, command: str,
519
608
  tokens: list, deferred_tokens: list
520
609
  ) -> int:
521
610
  '''Returns retcode (int). Creates a new Command object, presumably using a different tool,
522
611
 
523
612
  due to args changes from DEPS parsing that led to --tool=<different value> vs the automatic
524
- value if --tool was not originally set. Will run process_tokens(..) on the new sub commmand
613
+ value if --tool was not originally set. Will run process_tokens(..) on the new commmand
525
614
  object.
526
615
  '''
527
616
 
528
- use_tool = sco.args.get('tool', '')
617
+ use_tool = cmd_obj.args.get('tool', '')
529
618
 
530
619
  if not use_tool:
531
620
  util.error(f'Unable to change tool from {parsed.tool}, internal eda problem.')
@@ -536,8 +625,8 @@ def respawn_new_sub_command_object(
536
625
  f'{Colors.byellow}{command}')
537
626
 
538
627
  # Update the command handler(s) with this new tool. We don't really respawn, just
539
- # try to swap out the sco (Command obj handle)
540
- entry = config['auto_tools_order'][0].get(use_tool, {})
628
+ # try to swap out the cmd_obj (Command obj handle)
629
+ entry = config['tools'].get(use_tool, {})
541
630
  tool_cmd_handler_dict = entry.get('handlers', {})
542
631
 
543
632
  for _command, str_class_name in tool_cmd_handler_dict.items():
@@ -558,64 +647,81 @@ def respawn_new_sub_command_object(
558
647
  util.debug(f'Setting {cls=} for command={_command} in config.command_handler')
559
648
  config['command_handler'][_command] = cls
560
649
 
561
- old_sco = sco
562
- sco = config['command_handler'][command](config=config) # sub command object
563
- util.debug(f'No longer using handler: {type(old_sco)}; now using: {type(sco)}')
564
- sco.config['eda_original_args'] = old_sco.config['eda_original_args']
565
- del old_sco
650
+ old_cmd_obj = cmd_obj
651
+ cmd_obj = config['command_handler'][command](config=config) # command object
652
+ util.debug(f'No longer using handler: {type(old_cmd_obj)}; now using: {type(cmd_obj)}')
653
+ cmd_obj.config['eda_original_args'] = old_cmd_obj.config['eda_original_args']
654
+ del old_cmd_obj
566
655
 
567
- rc = check_command_handler_cls(command_obj=sco, command=command, parsed_args=parsed)
656
+ rc = check_command_handler_cls(command_obj=cmd_obj, command=command, parsed_args=parsed)
568
657
  if rc > 0:
569
- util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=},'
658
+ util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(cmd_obj)=},'
570
659
  f'unparsed={deferred_tokens}')
571
660
  return rc
572
661
 
573
- setattr(sco, 'command_name', command) # as a safeguard, 'command' set in 'sco'
662
+ setattr(cmd_obj, 'command_name', command) # as a safeguard, 'command' set in 'cmd_obj'
574
663
  util.info(f'--tool={use_tool}: running command: {Colors.byellow}eda {command} ',
575
664
  ' '.join(deferred_tokens))
576
- unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
665
+ unparsed = cmd_obj.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
577
666
 
578
667
  # query the status from the Command object (0 is pass, > 0 is fail, but we'd prefer to
579
668
  # avoid rc=1 because that's the python exception rc)
580
- rc = getattr(sco, 'status', 2)
581
- util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
669
+ rc = getattr(cmd_obj, 'status', 2)
670
+ util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(cmd_obj)=}, {unparsed=}')
582
671
  return rc
583
672
 
584
673
 
585
- def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> int:
674
+ def check_command_handler_cls(command_obj: object, command: str, parsed_args) -> int:
586
675
  '''Returns bash/sh return code, checks that a command handling class has all
587
676
 
588
677
  internal CHECK_REQUIRES list items. For example, sim.py has CHECK_REQUIRES=[Tool],
589
678
  so if a 'sim' command handler does not also inherit a Tool class, then reports this as an
590
679
  error.
591
680
  '''
592
- sco = command_obj
593
- for cls in getattr(sco, 'CHECK_REQUIRES', []):
594
- if not isinstance(sco, cls):
595
- # If someone set --tool verilator for command=synth, then our 'sco' will have defaulted
596
- # to CommandSynth with no tool attached. If we don't have a tool set, error and return.
681
+ cmd_obj = command_obj
682
+ for cls in getattr(cmd_obj, 'CHECK_REQUIRES', []):
683
+ if not isinstance(cmd_obj, cls):
684
+ # If someone set --tool verilator for command=synth, then our 'cmd_obj' will have
685
+ # defaulted to CommandSynth with no tool attached. If we don't have a tool set, and one
686
+ # is requires, error and return.
597
687
  parsed_tool = getattr(parsed_args, 'tool', '')
598
- auto_tool_entry = command_obj.config.get(
599
- 'auto_tools_order', [{}])[0].get(parsed_tool, {})
600
- if parsed_tool and not auto_tool_entry:
688
+ tool_entry = command_obj.config.get(
689
+ 'tools', {}).get(parsed_tool, {})
690
+ if parsed_tool and not tool_entry:
601
691
  util.warning(
602
- f"{command=} for tool '{parsed_tool}' is using handling class '{type(sco)}',",
603
- f"but missing requirement {cls}, likely because the tool was not loaded",
604
- "(not in PATH) or mis-configured (such as missing a Tool based class)"
692
+ f"{command=} for tool '{parsed_tool}' is using handling class",
693
+ f"'{type(cmd_obj)}', but missing requirement {cls}, likely because the tool",
694
+ "was not loaded (not in PATH) or mis-configured (such as missing a Tool based",
695
+ "class)"
605
696
  )
606
697
  return util.error(
607
698
  f"EDA {command=} for tool '{parsed_tool}' cannot be run because tool",
608
699
  f"'{parsed_tool}' is not known to `eda`. It does not exist in the config:",
609
700
  "see informational message for --config-yml, and check that file's",
610
- "auto_tools_order."
701
+ "tools config."
611
702
  )
612
703
  if parsed_tool:
704
+ # Does the tool_entry even have handlers for this command?
705
+ if command not in tool_entry.get('handlers', {}):
706
+ util.warning(
707
+ f"{command=} for tool '{parsed_tool}' does not have a handler defined in",
708
+ "the config: see informational message for --config-yml, and check that",
709
+ f"file's tools config. Known handlers for {parsed_tool} are: ",
710
+ f"{tool_entry.get('handlers', '')}"
711
+ )
712
+ return util.error(
713
+ f"{command=} for tool '{parsed_tool}' does not have a handler defined in",
714
+ "the config"
715
+ )
716
+
717
+ # It does have a handler defined, maybe it wasn't loaded due to other reasons?
613
718
  util.warning(
614
- f"{command=} for tool '{parsed_tool}' is using handling class '{type(sco)}',",
615
- f"but missing requirement {cls}, likely because the tool was not loaded",
616
- "(not in PATH) or mis-configured (such as missing a Tool based class)"
719
+ f"{command=} for tool '{parsed_tool}' is using handling class",
720
+ f"'{type(cmd_obj)}', but missing requirement {cls}, likely because the tool",
721
+ "was not loaded (not in PATH) or mis-configured (such as missing a Tool based",
722
+ "class)"
617
723
  )
618
- for k,v in auto_tool_entry.items():
724
+ for k,v in tool_entry.items():
619
725
  if k == 'exe' or k.startswith('requires_cmd'):
620
726
  util.warning(
621
727
  f"tool '{parsed_tool}' has requirements that may not have been met --",
@@ -638,8 +744,8 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
638
744
  # No parsed_tool.
639
745
  util.warning(
640
746
  f"{command=} for default tool (--tool not set) is using handling class",
641
- f"'{type(sco)}', but missing requirement {cls}, likely because the tool was not",
642
- "loaded (not in PATH) or mis-configured (such as missing a Tool based class)"
747
+ f"'{type(cmd_obj)}', but missing requirement {cls}, likely because the tool was",
748
+ "not loaded (not in PATH) or mis-configured (such as missing a Tool based class)"
643
749
  )
644
750
  return util.error(
645
751
  f"EDA {command=} for default tool (--tool not set) is not supported (default",
@@ -692,6 +798,9 @@ def main(*args):
692
798
  # And show the command that was run (all args):
693
799
  util.info(f"main: {Colors.byellow}eda {' '.join(args)}{Colors.normal}{Colors.green};",
694
800
  f"(run from {os.getcwd()})")
801
+ # And show python version:
802
+ util.info(f'python: version {sys.version_info.major}.{sys.version_info.minor}.'
803
+ f'{sys.version_info.micro}')
695
804
 
696
805
  # Handle --config-yml= arg
697
806
  config, unparsed = eda_config.get_eda_config(unparsed)