opencos-eda 0.2.52__py3-none-any.whl → 0.2.54__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 (44) 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 +4 -1
  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 +76 -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 +177 -33
  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 +14 -3
  25. opencos/tests/test_tools.py +159 -132
  26. opencos/tools/cocotb.py +15 -14
  27. opencos/tools/iverilog.py +4 -24
  28. opencos/tools/modelsim_ase.py +70 -57
  29. opencos/tools/quartus.py +680 -0
  30. opencos/tools/questa.py +158 -90
  31. opencos/tools/questa_fse.py +10 -0
  32. opencos/tools/riviera.py +1 -0
  33. opencos/tools/verilator.py +9 -15
  34. opencos/tools/vivado.py +30 -23
  35. opencos/util.py +89 -15
  36. opencos/utils/status_constants.py +1 -0
  37. opencos/utils/str_helpers.py +85 -0
  38. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/METADATA +1 -1
  39. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/RECORD +44 -42
  40. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/WHEEL +0 -0
  41. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/entry_points.txt +0 -0
  42. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/licenses/LICENSE +0 -0
  43. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/licenses/LICENSE.spdx +0 -0
  44. {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/top_level.txt +0 -0
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
@@ -241,10 +252,12 @@ class Command:
241
252
  })
242
253
  self.modified_args = {}
243
254
  self.config = copy.deepcopy(config) # avoid external modifications.
244
- self.target = ""
255
+ self.target = "" # is set as the 'top' or final target short-name (no path info)
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.
@@ -838,6 +943,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
838
943
  self.targets_dict = {} # key = targets that we've already processed in DEPS files
839
944
  self.last_added_source_file_inferred_top = ''
840
945
 
946
+ self.has_dep_shell_commands = False
947
+
841
948
 
842
949
  def run_dep_commands(self) -> None:
843
950
  '''Run shell/peakrdl style commands from DEPS files
@@ -888,10 +995,12 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
888
995
 
889
996
  d['exec_list'] = clist # update to tee_fpath is set.
890
997
 
891
- util.write_shell_command_file(
892
- dirpath=self.args['work-dir'], filename='pre_compile_dep_shell_commands.sh',
893
- command_lists=all_cmds_lists
894
- )
998
+ if all_cmds_lists:
999
+ util.write_shell_command_file(
1000
+ dirpath=self.args['work-dir'], filename='pre_compile_dep_shell_commands.sh',
1001
+ command_lists=all_cmds_lists
1002
+ )
1003
+ self.has_dep_shell_commands = True
895
1004
 
896
1005
  for i, d in enumerate(self.dep_shell_commands):
897
1006
  util.info(f'run_dep_shell_commands {i=}: {d=}')
@@ -1345,10 +1454,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1345
1454
  plusarg = strip_outer_quotes(token)
1346
1455
  self.process_plusarg(plusarg, pwd=pwd)
1347
1456
  remove_list.append(token)
1457
+
1348
1458
  for x in remove_list:
1349
1459
  unparsed.remove(x)
1350
1460
 
1351
- if not unparsed and self.error_on_no_files_or_targets:
1461
+ if self.error_on_no_files_or_targets and not unparsed:
1352
1462
  # derived classes can set error_on_no_files_or_targets=True
1353
1463
  # For example: CommandSim will error (requires files/targets),
1354
1464
  # CommandWaves does not (files/targets not required)
@@ -1367,13 +1477,27 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1367
1477
  util.warning(f"For command '{self.command_name}' no files or targets were",
1368
1478
  f"presented at the command line, so using '{target}' from",
1369
1479
  f"{deps.deps_file}")
1370
- if len(unparsed) == 0:
1480
+ if not unparsed:
1371
1481
  # If unparsed is still empty, then error.
1372
1482
  self.error(f"For command '{self.command_name}' no files or targets were",
1373
1483
  f"presented at the command line: {orig_tokens}",
1374
1484
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1375
1485
 
1376
1486
  # by this point hopefully this is a target ... is it a simple filename?
1487
+
1488
+ # Before we look for files, check for stray --some-arg in unparsed, we don't want to treat
1489
+ # these as potential targets if process_all=True, but someone might have a file named
1490
+ # --my_file.sv, so those are technically allowed until the tool would fail on them.
1491
+ possible_unparsed_args = [
1492
+ x for x in unparsed if x.startswith('--') and not os.path.isfile(x)
1493
+ ]
1494
+ if process_all and possible_unparsed_args:
1495
+ _tool = self.safe_which_tool()
1496
+ self.warning_show_known_args()
1497
+ self.error(f"Didn't understand unparsed args: {possible_unparsed_args}, for command",
1498
+ f"'{self.command_name}', tool '{_tool}'",
1499
+ error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1500
+
1377
1501
  remove_list = []
1378
1502
  last_potential_top_file = ('', '') # (top, fpath)
1379
1503
  last_potential_top_target = ('', '') # (top, path/to/full-target-name)
@@ -1405,6 +1529,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1405
1529
 
1406
1530
  # we appear to be dealing with a target name which needs to be resolved (usually
1407
1531
  # recursively)
1532
+ if token.startswith('-'):
1533
+ # We are not going to handle targets that start with a -, it's likely
1534
+ # an unparsed arg.
1535
+ continue
1408
1536
  if token.startswith(os.sep):
1409
1537
  target_name = token # if it's absolute path, don't prepend anything
1410
1538
  else:
@@ -1424,17 +1552,20 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1424
1552
  unparsed.remove(x)
1425
1553
 
1426
1554
  # 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",
1555
+ if process_all and unparsed:
1556
+ self.warning_show_known_args()
1557
+ self.error(f"Didn't understand remaining args or targets {unparsed=} for command",
1558
+ f"'{self.command_name}'",
1429
1559
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1430
1560
 
1431
1561
  # handle a missing self.args['top'] with last filepath or last target:
1432
1562
  if not self.args.get('top', ''):
1563
+ top_path = ''
1433
1564
  if not last_potential_top_isfile and last_potential_top_target[0]:
1434
1565
  # If we have a target name from DEPS, prefer to use that.
1435
- self.args['top'], self.args['top-path'] = last_potential_top_target
1566
+ self.args['top'], top_path = last_potential_top_target
1436
1567
  util.info("--top not specified, inferred from target:",
1437
- f"{self.args['top']} ({self.args['top-path']})")
1568
+ f"{self.args['top']} ({top_path})")
1438
1569
 
1439
1570
  else:
1440
1571
  best_top_fname = self.last_added_source_file_inferred_top
@@ -1444,24 +1575,35 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1444
1575
  if not self.args['top'] and last_potential_top_file[0]:
1445
1576
  # If we don't have a target name, and no top name yet, then go looking for the
1446
1577
  # module name in the final source file added.
1447
- self.args['top-path'] = last_potential_top_file[1] # from tuple: (top, fpath)
1578
+ top_path = last_potential_top_file[1] # from tuple: (top, fpath)
1448
1579
  self.args['top'] = util.get_inferred_top_module_name(
1449
1580
  module_guess=last_potential_top_file[0],
1450
1581
  module_fpath=last_potential_top_file[1]
1451
1582
  )
1452
1583
  if self.args['top']:
1453
1584
  util.info("--top not specified, inferred from final source file:",
1454
- f"{self.args['top']} ({self.args['top-path']})")
1585
+ f"{self.args['top']} ({top_path})")
1455
1586
  # If top wasn't set, and we're using the final command-line 'arg' filename
1456
1587
  # (not from DEPS.yml) we need to override self.target if that was set. Otherwise
1457
1588
  # it won't save to the correct work-dir:
1458
1589
  self.target = self.args['top']
1459
1590
 
1591
+
1592
+ util.info(f'{self.command_name}: top-most target name: {self.target}')
1593
+
1460
1594
  if self.error_on_missing_top and not self.args.get('top', ''):
1461
1595
  self.error("Did not get a --top or DEPS top, required to run command",
1462
1596
  f"'{self.command_name}' for tool={self.args.get('tool', None)}",
1463
1597
  error_code=status_constants.EDA_COMMAND_MISSING_TOP)
1464
1598
 
1599
+ if self.tool_changed_respawn:
1600
+ util.info(
1601
+ 'CommandDesign: need to respawn due to tool change to',
1602
+ f'\'{self.tool_changed_respawn["tool"]}\' from',
1603
+ f'\'{self.tool_changed_respawn["orig_tool"]}\'',
1604
+ f'(from DEPS, {self.tool_changed_respawn["from"]})'
1605
+ )
1606
+
1465
1607
  return unparsed
1466
1608
 
1467
1609
 
@@ -1481,7 +1623,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1481
1623
  for k,v in self.args.items():
1482
1624
 
1483
1625
  # Some args cannot be extracted and work, so omit these:
1484
- if k in ['top-path'] + remove_args:
1626
+ if k in remove_args:
1485
1627
  continue
1486
1628
  if any(k.startswith(x) for x in remove_args_startswith):
1487
1629
  continue
@@ -1503,9 +1645,6 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1503
1645
  return ret
1504
1646
 
1505
1647
 
1506
- #_THREADS_START = 0
1507
- #_THREADS_DONE = 0
1508
-
1509
1648
  class ThreadStats:
1510
1649
  '''To avoid globals for two ints, keep a class holder so CommandParallel and
1511
1650
  CommandParallelWorker can share values'''
@@ -1961,6 +2100,7 @@ class CommandParallel(Command):
1961
2100
  # There should not be any single_cmd_unparsed args starting with '-'
1962
2101
  bad_remaining_args = [x for x in single_cmd_unparsed if x.startswith('-')]
1963
2102
  if bad_remaining_args:
2103
+ self.warning_show_known_args(command=f'{self.command_name} {command}')
1964
2104
  self.error(f'for {self.command_name} {command=} the following args are unknown',
1965
2105
  f'{bad_remaining_args}',
1966
2106
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
@@ -2020,8 +2160,12 @@ class CommandParallel(Command):
2020
2160
  tpath, _ = os.path.split(job_dict['target'])
2021
2161
 
2022
2162
  # 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}'
2163
+ patched_target_path = os.path.relpath(tpath).replace(os.sep, '_').lstrip('.')
2164
+ if patched_target_path:
2165
+ new_job_name = f'{patched_target_path}.{key}'
2166
+ else:
2167
+ continue # there's nothing to "patch", our job-name will be unchanged.
2168
+
2025
2169
  replace_job_arg(job_dict, arg_name='job-name', new_value=new_job_name)
2026
2170
 
2027
2171
  # 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',
@@ -20,11 +20,33 @@ DEFAULT_HANDLERS:
20
20
  sweep : opencos.commands.CommandSweep
21
21
  flist : opencos.commands.CommandFList
22
22
  # These commands (waves, export, targets) do not require a tool, or
23
- # will self determine the tool:
23
+ # will self determine the tool. See command_tool_is_optional in this config file.
24
24
  waves : opencos.commands.CommandWaves
25
25
  export : opencos.commands.CommandExport
26
26
  shell : opencos.commands.CommandShell
27
27
  targets : opencos.commands.CommandTargets
28
+ deps-help : opencos.commands.CommandDepsHelp
29
+
30
+ DEFAULT_HANDLERS_HELP:
31
+ sim: Simulates a DEPS target.
32
+ elab: Elaborates a DEPS target (lint, tool specific).
33
+ synth: Synthesizes a DEPS target.
34
+ flist: Create dependency from a DEPS target.
35
+ proj: Create a project from a DEPS target for GUI sim/waves/debug.
36
+ multi: Run multiple DEPS targets, serially or in parallel.
37
+ tools-multi: Same as 'multi' but run on all available tools, or specfied using --tools.
38
+ sweep: Sweep one or more arguments across a range, serially or in parallel.
39
+ build: Build for a board, creating a project and running build flow.
40
+ waves: Opens waveform from prior simulation.
41
+ upload: Uploads a finished design into hardware.
42
+ open: Opens a project.
43
+ export: Export files related to a target, tool independent.
44
+ shell: Runs only commands for DEPS target (like sim or elab, but stops prior to tool).
45
+ targets: List all possible targets given glob path.
46
+ lec: Run equivalence on two designs.
47
+ deps-help: Provide help about DEPS markup files, or schema using --verbose or --help.
48
+ help: This help (without args), or i.e. "eda help sim" for specific help.
49
+
28
50
 
29
51
 
30
52
  defines: { } # Add these defines to every eda call
@@ -40,6 +62,7 @@ dep_command_enables:
40
62
  dep_tags_enables:
41
63
  with-tools: true
42
64
  with-args: true
65
+ with-commands: true
43
66
  args: true
44
67
  replace-config-tools: true
45
68
  additive-config-tools: true
@@ -83,6 +106,7 @@ command_tool_is_optional:
83
106
  - flist
84
107
  - export
85
108
  - targets
109
+ - deps-help
86
110
 
87
111
 
88
112
  tools:
@@ -234,6 +258,9 @@ tools:
234
258
  - 2555 # 2555 - assignment to input port myname
235
259
  - 2583 # 2583 - [SVCHK] - Extra checking for conflicts with always_comb and
236
260
  # always_latch variables is done at vopt time.
261
+ - 13159 # 13159, 2685, 2718 are all related to module instance port default values.
262
+ - 2685
263
+ - 2718
237
264
  simulate-waivers:
238
265
  - 3009 # 3009: [TSCALE] - Module 'myname' does not have a timeunit/timeprecision
239
266
  # specification in effect, but other modules do.
@@ -256,6 +283,9 @@ tools:
256
283
  - 2555 # 2555 - assignment to input port myname
257
284
  - 2583 # 2583 - [SVCHK] - Extra checking for conflicts with always_comb and
258
285
  # always_latch variables is done at vopt time.
286
+ - 13159 # 13159, 2685, 2718 are all related to module instance port default values.
287
+ - 2685
288
+ - 2718
259
289
  simulate-waivers:
260
290
  - 3009 # 3009: [TSCALE] - Module 'myname' does not have a timeunit/timeprecision
261
291
  # specification in effect, but other modules do.
@@ -295,6 +325,10 @@ tools:
295
325
  - "Cocotb test completed successfully!"
296
326
 
297
327
 
328
+ quartus:
329
+ defines:
330
+ OC_TOOL_QUARTUS: null
331
+
298
332
  vivado:
299
333
  sim-libraries:
300
334
  - xil_defaultlib
@@ -375,6 +409,16 @@ auto_tools_order:
375
409
  exe: gtkwave
376
410
  handlers: { }
377
411
 
412
+ quartus:
413
+ exe: quartus_sh
414
+ handlers:
415
+ synth: opencos.tools.quartus.CommandSynthQuartus
416
+ build: opencos.tools.quartus.CommandBuildQuartus
417
+ flist: opencos.tools.quartus.CommandFListQuartus
418
+ proj: opencos.tools.quartus.CommandProjQuartus
419
+ upload: opencos.tools.quartus.CommandUploadQuartus
420
+ open: opencos.tools.quartus.CommandOpenQuartus
421
+
378
422
  vivado:
379
423
  exe: vivado
380
424
  handlers:
@@ -425,8 +469,9 @@ auto_tools_order:
425
469
  exe: qrun
426
470
  requires_vsim_helper: True
427
471
  handlers:
428
- elab: opencos.tools.queta.CommandElabQuesta
429
- sim: opencos.tools.queta.CommandSimQuesta
472
+ elab: opencos.tools.questa.CommandElabQuesta
473
+ sim: opencos.tools.questa.CommandSimQuesta
474
+ flist: opencos.tools.questa.CommandFListQuesta
430
475
 
431
476
  riviera:
432
477
  exe: vsim
@@ -450,6 +495,7 @@ auto_tools_order:
450
495
  handlers:
451
496
  elab: opencos.tools.questa_fse.CommandElabQuestaFse
452
497
  sim: opencos.tools.questa_fse.CommandSimQuestaFse
498
+ flist: opencos.tools.questa_fse.CommandFListQuestaFse
453
499
 
454
500
  iverilog:
455
501
  exe: iverilog
@@ -10,68 +10,11 @@ import os
10
10
  from pathlib import Path
11
11
 
12
12
  from opencos.deps.deps_file import get_all_targets
13
+ from opencos.utils.str_helpers import print_columns_manual
13
14
 
14
15
  PATH_LPREFIX = str(Path('.')) + os.path.sep
15
16
 
16
17
 
17
- def get_terminal_columns():
18
- """
19
- Retrieves the number of columns (width) of the terminal window.
20
-
21
- Returns:
22
- int: The number of columns in the terminal, or a default value (e.g., 80)
23
- if the terminal size cannot be determined.
24
- """
25
- try:
26
- size = os.get_terminal_size()
27
- return size.columns
28
- except OSError:
29
- # Handle cases where the terminal size cannot be determined (e.g., not in a TTY)
30
- return 80 # Default to 80 columns
31
-
32
- return 80 # else default to 80.
33
-
34
-
35
- def print_columns_manual(data: list, num_columns: int = 4, auto_columns: bool = True) -> None:
36
- """Prints a list of strings in columns, manually aligning them."""
37
-
38
- if not data:
39
- print()
40
- return
41
-
42
- _spacing = 2
43
-
44
- # Calculate maximum width for each column
45
- max_lengths = [0] * num_columns
46
- max_item_len = 0
47
- for i, item in enumerate(data):
48
- col_index = i % num_columns
49
- max_lengths[col_index] = max(max_lengths[col_index], len(item))
50
- max_item_len = max(max_item_len, len(item))
51
-
52
- if auto_columns and num_columns > 1:
53
- window_cols = get_terminal_columns()
54
- max_line_len = 0
55
- for x in max_lengths:
56
- max_line_len += x + _spacing
57
- if max_line_len > window_cols:
58
- # subtract a column (already >= 2):
59
- print_columns_manual(data=data, num_columns=num_columns-1, auto_columns=True)
60
- return
61
- if max_line_len + max_item_len + _spacing < window_cols:
62
- # add 1 more column if we're guaranteed to have room.
63
- print_columns_manual(data=data, num_columns=num_columns+1, auto_columns=True)
64
- return
65
- # else continue
66
-
67
- # Print data in columns
68
- for i, item in enumerate(data):
69
- col_index = i % num_columns
70
- print(item.ljust(max_lengths[col_index] + _spacing), end="") # Add padding
71
- if col_index == num_columns - 1 or i == len(data) - 1:
72
- print() # New line at the end of a row or end of data
73
-
74
-
75
18
  def get_path_and_pattern(partial_path: str = '', base_path=str(Path('.'))) -> (str, str):
76
19
  '''Returns tuple of (partial_path, partial_target or filter)'''
77
20
 
opencos/tests/helpers.py CHANGED
@@ -12,6 +12,22 @@ from contextlib import redirect_stdout, redirect_stderr
12
12
  from opencos import eda
13
13
  from opencos import deps_schema
14
14
  from opencos.utils.markup_helpers import yaml_safe_load
15
+ from opencos.utils import status_constants
16
+
17
+
18
+ def eda_wrap_is_sim_fail(rc: int, quiet: bool = False) -> bool:
19
+ '''Because eda_wrap calls eda_main(..) and will continue running
20
+
21
+ after the first error, we may get a higher return code.'''
22
+ if not quiet:
23
+ print(f'eda_wrap_is_sim_fail({rc=})')
24
+ return rc in (
25
+ status_constants.EDA_COMMAND_MISSING_TOP,
26
+ status_constants.EDA_SIM_LOG_HAS_BAD_STRING,
27
+ status_constants.EDA_SIM_LOG_MISSING_MUST_STRING,
28
+ status_constants.EDA_EXEC_NONZERO_RETURN_CODE2,
29
+ status_constants.EDA_DEFAULT_ERROR
30
+ )
15
31
 
16
32
  def can_run_eda_command(*commands, config: dict) -> bool:
17
33
  '''Returns True if we have any installed tool that can run: eda <command>'''