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

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