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.
- opencos/commands/__init__.py +2 -0
- opencos/commands/build.py +1 -1
- opencos/commands/deps_help.py +259 -0
- opencos/commands/export.py +1 -1
- opencos/commands/flist.py +4 -1
- opencos/commands/lec.py +1 -1
- opencos/commands/open.py +2 -0
- opencos/commands/proj.py +1 -1
- opencos/commands/shell.py +1 -1
- opencos/commands/sim.py +76 -8
- opencos/commands/synth.py +1 -1
- opencos/commands/upload.py +3 -0
- opencos/commands/waves.py +1 -0
- opencos/deps/defaults.py +1 -0
- opencos/deps/deps_file.py +30 -4
- opencos/deps/deps_processor.py +72 -2
- opencos/deps_schema.py +3 -0
- opencos/eda.py +50 -26
- opencos/eda_base.py +177 -33
- opencos/eda_config.py +1 -1
- opencos/eda_config_defaults.yml +49 -3
- opencos/eda_extract_targets.py +1 -58
- opencos/tests/helpers.py +16 -0
- opencos/tests/test_eda.py +14 -3
- opencos/tests/test_tools.py +159 -132
- opencos/tools/cocotb.py +15 -14
- opencos/tools/iverilog.py +4 -24
- opencos/tools/modelsim_ase.py +70 -57
- opencos/tools/quartus.py +680 -0
- opencos/tools/questa.py +158 -90
- opencos/tools/questa_fse.py +10 -0
- opencos/tools/riviera.py +1 -0
- opencos/tools/verilator.py +9 -15
- opencos/tools/vivado.py +30 -23
- opencos/util.py +89 -15
- opencos/utils/status_constants.py +1 -0
- opencos/utils/str_helpers.py +85 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/METADATA +1 -1
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/RECORD +44 -42
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/WHEEL +0 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/entry_points.txt +0 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.54.dist-info}/licenses/LICENSE.spdx +0 -0
- {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
|
|
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
|
-
'
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
|
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
|
|
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
|
|
1428
|
-
self.
|
|
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'],
|
|
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']} ({
|
|
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
|
-
|
|
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']} ({
|
|
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
|
|
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
|
-
|
|
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
opencos/eda_config_defaults.yml
CHANGED
|
@@ -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.
|
|
429
|
-
sim: opencos.tools.
|
|
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
|
opencos/eda_extract_targets.py
CHANGED
|
@@ -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>'''
|