opencos-eda 0.3.5__py3-none-any.whl → 0.3.7__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/flist.py +2 -2
- opencos/commands/multi.py +4 -3
- opencos/commands/sim.py +28 -9
- opencos/deps/deps_processor.py +13 -2
- opencos/eda.py +23 -20
- opencos/eda_base.py +224 -90
- opencos/eda_config.py +2 -1
- opencos/eda_config_defaults.yml +5 -1
- opencos/export_helper.py +89 -31
- opencos/files.py +3 -1
- opencos/tests/deps_files/command_order/DEPS.yml +13 -0
- opencos/tests/helpers.py +32 -22
- opencos/tests/test_eda.py +1 -1
- opencos/tests/test_tools.py +9 -3
- opencos/tools/cocotb.py +94 -21
- opencos/tools/verilator.py +6 -4
- opencos/tools/yosys.py +3 -3
- opencos/util.py +100 -55
- opencos/utils/subprocess_helpers.py +23 -6
- {opencos_eda-0.3.5.dist-info → opencos_eda-0.3.7.dist-info}/METADATA +5 -2
- {opencos_eda-0.3.5.dist-info → opencos_eda-0.3.7.dist-info}/RECORD +26 -26
- {opencos_eda-0.3.5.dist-info → opencos_eda-0.3.7.dist-info}/WHEEL +0 -0
- {opencos_eda-0.3.5.dist-info → opencos_eda-0.3.7.dist-info}/entry_points.txt +0 -0
- {opencos_eda-0.3.5.dist-info → opencos_eda-0.3.7.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.3.5.dist-info → opencos_eda-0.3.7.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.3.5.dist-info → opencos_eda-0.3.7.dist-info}/top_level.txt +0 -0
opencos/eda_base.py
CHANGED
|
@@ -24,9 +24,9 @@ from pathlib import Path
|
|
|
24
24
|
from opencos import seed, util, files
|
|
25
25
|
from opencos import eda_config
|
|
26
26
|
|
|
27
|
-
from opencos.util import Colors
|
|
27
|
+
from opencos.util import Colors, safe_emoji
|
|
28
28
|
from opencos.utils.str_helpers import sprint_time, strip_outer_quotes, string_or_space, \
|
|
29
|
-
indent_wrap_long_text, pretty_list_columns_manual
|
|
29
|
+
indent_wrap_long_text, pretty_list_columns_manual, get_terminal_columns
|
|
30
30
|
from opencos.utils.subprocess_helpers import subprocess_run_background
|
|
31
31
|
from opencos.utils import status_constants
|
|
32
32
|
|
|
@@ -42,9 +42,29 @@ def print_base_help() -> None:
|
|
|
42
42
|
print(get_argparser_short_help())
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def print_eda_usage_line(no_targets: bool = False, command_name='COMMAND') -> None:
|
|
46
|
+
'''Prints line for eda [options] COMMAND [options] FILES|TARGETS,...'''
|
|
47
|
+
print(f'{safe_emoji("🔦 ")}Usage:')
|
|
48
|
+
if no_targets:
|
|
49
|
+
print(
|
|
50
|
+
(f' {Colors.bold}{Colors.yellow}eda {Colors.cyan}[options]'
|
|
51
|
+
f' {Colors.yellow}{command_name} {Colors.cyan}[options]{Colors.normal}')
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
print(
|
|
55
|
+
(f' {Colors.bold}{Colors.yellow}eda {Colors.cyan}[options]'
|
|
56
|
+
f' {Colors.yellow}{command_name} {Colors.cyan}[options]'
|
|
57
|
+
f' {Colors.yellow}FILES|TARGETS,...{Colors.normal}')
|
|
58
|
+
)
|
|
59
|
+
print()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
45
63
|
def get_argparser() -> argparse.ArgumentParser:
|
|
46
64
|
'''Returns the ArgumentParser for general eda CLI'''
|
|
47
|
-
parser = argparse.ArgumentParser(
|
|
65
|
+
parser = argparse.ArgumentParser(
|
|
66
|
+
prog=f'{safe_emoji("🔎 ")}eda options', add_help=False, allow_abbrev=False
|
|
67
|
+
)
|
|
48
68
|
parser.add_argument('-q', '--quit', action='store_true',
|
|
49
69
|
help=(
|
|
50
70
|
'For interactive mode (eda called with no options, command, or'
|
|
@@ -80,7 +100,7 @@ def get_argparsers_args_list() -> list:
|
|
|
80
100
|
|
|
81
101
|
|
|
82
102
|
def get_eda_exec(command: str = '') -> str:
|
|
83
|
-
'''Returns the full path of `eda` executable to be used for a given eda
|
|
103
|
+
'''Returns the full path of `eda` executable to be used for a given eda COMMAND'''
|
|
84
104
|
# NOTE(drew): This is kind of flaky. 'eda multi' reinvokes 'eda'. But the executable for 'eda'
|
|
85
105
|
# is one of:
|
|
86
106
|
# 1. pip3 install opencos-eda
|
|
@@ -131,6 +151,17 @@ def which_tool(command: str, config: dict) -> str:
|
|
|
131
151
|
return tool
|
|
132
152
|
|
|
133
153
|
|
|
154
|
+
def get_class_tool_name(command_obj: object) -> str:
|
|
155
|
+
'''Attempts to return command_obj._TOOL via command_obj.get_tool_name()'''
|
|
156
|
+
if f := getattr(command_obj, 'get_tool_name', None):
|
|
157
|
+
if callable(f):
|
|
158
|
+
ret = f()
|
|
159
|
+
if ret and isinstance(ret, str):
|
|
160
|
+
return ret
|
|
161
|
+
return ''
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
134
165
|
class Tool:
|
|
135
166
|
'''opencos.eda_base.Tool is a base class used by opencos.tools.<name>.
|
|
136
167
|
|
|
@@ -180,6 +211,10 @@ class Tool:
|
|
|
180
211
|
util.info(f'Override for {self._TOOL} using exe {exe}')
|
|
181
212
|
self._EXE = exe
|
|
182
213
|
|
|
214
|
+
def get_tool_name(self) -> str:
|
|
215
|
+
'''Returns _TOOL'''
|
|
216
|
+
return str(self._TOOL)
|
|
217
|
+
|
|
183
218
|
def get_full_tool_and_versions(self) -> str:
|
|
184
219
|
'''Returns tool:version, such as: verilator:5.033'''
|
|
185
220
|
if not self._VERSION:
|
|
@@ -196,7 +231,7 @@ class Tool:
|
|
|
196
231
|
|
|
197
232
|
|
|
198
233
|
class Command: # pylint: disable=too-many-public-methods
|
|
199
|
-
'''Base class for all: eda
|
|
234
|
+
'''Base class for all: eda COMMAND
|
|
200
235
|
|
|
201
236
|
The Command class should be used when you don't require files, otherwise consider
|
|
202
237
|
CommandDesign.
|
|
@@ -233,21 +268,54 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
233
268
|
'error-unknown-args': True,
|
|
234
269
|
})
|
|
235
270
|
self.args_help.update({
|
|
236
|
-
'
|
|
237
|
-
|
|
238
|
-
|
|
271
|
+
'keep': (
|
|
272
|
+
'Determined by eda <tool>, generally prevents jobs from overwriting artifacts'
|
|
273
|
+
),
|
|
274
|
+
'force': 'Determined by eda <tool>, force override',
|
|
275
|
+
'fake': (
|
|
276
|
+
'Determined by eda <tool>, generally is a dry-run that does not execute tool'
|
|
277
|
+
),
|
|
278
|
+
'stop-before-compile': (
|
|
279
|
+
'stop this run before any compile steps (if possible for tool) and'
|
|
280
|
+
' save .sh scripts in eda-dir/'
|
|
281
|
+
),
|
|
282
|
+
'stop-after-compile': (
|
|
283
|
+
'stop this run after any compile steps (if possible for tool) and'
|
|
284
|
+
' save .sh scripts in eda-dir/'
|
|
285
|
+
),
|
|
286
|
+
'stop-after-elaborate': (
|
|
287
|
+
'stop this run after any elaborate steps (if possible for tool) and'
|
|
288
|
+
' save .sh scripts in eda-dir/'
|
|
289
|
+
),
|
|
290
|
+
'lint': 'tool dependent, run with linting options',
|
|
291
|
+
"job-name" : 'Optional, used to create a sub directory under base work-dir (eda-dir)',
|
|
292
|
+
"sub-work-dir" : (
|
|
293
|
+
'Optional, similar to job-name, can be used to name the directory created under'
|
|
294
|
+
' eda-dir'
|
|
295
|
+
),
|
|
296
|
+
'eda-dir': 'Optional, relative base directory where eda logs are saved',
|
|
239
297
|
'export': 'export results for these targets in eda-dir',
|
|
240
298
|
'export-run': 'export, and run, results for these targets in eda-dir',
|
|
241
299
|
'export-json': 'export, and save a JSON file per target',
|
|
242
|
-
'work-dir': ('Optional override for working directory, often
|
|
243
|
-
' ./eda.work
|
|
300
|
+
'work-dir': ('Optional override for working directory, if unset will often use '
|
|
301
|
+
' ./eda.work/[TOP|TARGET].COMMAND'),
|
|
244
302
|
"work-dir-use-target-dir": ('Set the work-dir to be the same as the in-place location'
|
|
245
303
|
' where the target (DEPS) exists'),
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
304
|
+
"suffix": (
|
|
305
|
+
"Optional, determined by eda COMMAND, used by 'multi' jobs information logging"
|
|
306
|
+
),
|
|
307
|
+
"design": (
|
|
308
|
+
"Optional, used to override both the work-dir, and if unset the top DEPS target"
|
|
309
|
+
),
|
|
310
|
+
'enable-tags': (
|
|
311
|
+
'DEPS markup tag names to be force enabled for this'
|
|
312
|
+
' command (mulitple appends to list).'
|
|
313
|
+
),
|
|
314
|
+
'disable-tags': (
|
|
315
|
+
'DEPS markup tag names to be disabled (even if they match the criteria) for this'
|
|
316
|
+
' command (mulitple appends to list). --disable-tags has higher precedence than'
|
|
317
|
+
' --enable-tags.'
|
|
318
|
+
),
|
|
251
319
|
'test-mode': ('command and tool dependent, usually stops the command early without'
|
|
252
320
|
' executing.'),
|
|
253
321
|
'error-unknown-args': (
|
|
@@ -313,6 +381,18 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
313
381
|
util.error(f"command '{self.command_name}' has previous errors")
|
|
314
382
|
return self.status > 0
|
|
315
383
|
|
|
384
|
+
def report_pass_fail(self) -> None:
|
|
385
|
+
'''Reports an INFO line with pass/fail information'''
|
|
386
|
+
job_name = ' - '.join(
|
|
387
|
+
x for x in (self.command_name, self.args.get('tool', ''),
|
|
388
|
+
self.args.get('top', '')) if x
|
|
389
|
+
)
|
|
390
|
+
if self.status_any_error():
|
|
391
|
+
util.info(f'{safe_emoji("❌ ")}{job_name}: Errors observed.', color=Colors.red)
|
|
392
|
+
else:
|
|
393
|
+
util.info(f'{safe_emoji("✅ ")}{job_name}: No errors observed.')
|
|
394
|
+
|
|
395
|
+
|
|
316
396
|
def which_tool(self, command:str) -> str:
|
|
317
397
|
'''Returns a str for the tool name used for the requested command'''
|
|
318
398
|
return which_tool(command, config=self.config)
|
|
@@ -342,7 +422,7 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
342
422
|
) -> str:
|
|
343
423
|
'''Creates the working directory and populates self.args['work-dir']
|
|
344
424
|
|
|
345
|
-
Generally uses ./ self.args['eda-dir'] /
|
|
425
|
+
Generally uses ./ self.args['eda-dir'] / TARGET-NAME.COMMAND /
|
|
346
426
|
however, self.args['job-name'] or ['sub-work-dir'] can override that.
|
|
347
427
|
|
|
348
428
|
Additionally, the work-dir is attempted to be deleted if it already exists
|
|
@@ -462,7 +542,8 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
462
542
|
if not tee_fpath and getattr(command_list, 'tee_fpath', None):
|
|
463
543
|
tee_fpath = getattr(command_list, 'tee_fpath', '')
|
|
464
544
|
if not quiet:
|
|
465
|
-
util.info(f"exec: {' '.join(command_list)}
|
|
545
|
+
util.info(f"{safe_emoji('⏩ ')}exec: {' '.join(command_list)}",
|
|
546
|
+
f"(in {work_dir}, {tee_fpath=})")
|
|
466
547
|
|
|
467
548
|
stdout, stderr, return_code = subprocess_run_background(
|
|
468
549
|
work_dir=work_dir,
|
|
@@ -484,7 +565,8 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
484
565
|
self.error(f"exec: returned with error (return code: {return_code})",
|
|
485
566
|
error_code=self.status)
|
|
486
567
|
else:
|
|
487
|
-
util.debug(f"exec: returned with error (return code:
|
|
568
|
+
util.debug(f"{safe_emoji('❌ ')}exec: returned with error (return code:",
|
|
569
|
+
f"{return_code})")
|
|
488
570
|
else:
|
|
489
571
|
util.debug(f"exec: returned without error (return code: {return_code})")
|
|
490
572
|
return stderr, stdout, return_code
|
|
@@ -564,7 +646,9 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
564
646
|
# parsed.args-with-dashes is not legal python. Some of self.args.keys() still have - or _,
|
|
565
647
|
# so this will handle both.
|
|
566
648
|
# Also, preference is for self.args.keys(), to be str with - dashes
|
|
567
|
-
parser = argparse.ArgumentParser(
|
|
649
|
+
parser = argparse.ArgumentParser(
|
|
650
|
+
prog=f'{safe_emoji("🔎 ")}eda', add_help=False, allow_abbrev=False
|
|
651
|
+
)
|
|
568
652
|
bool_action_kwargs = util.get_argparse_bool_action_kwargs()
|
|
569
653
|
|
|
570
654
|
if not parser_arg_list:
|
|
@@ -601,20 +685,33 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
601
685
|
# is []. If the parsed Namespace has values set to None or [], we do not update. This
|
|
602
686
|
# means that as deps are processed that have args set, they cannot override the top
|
|
603
687
|
# level args that were already set, nor be overriden by defaults.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
688
|
+
try:
|
|
689
|
+
if isinstance(value, bool):
|
|
690
|
+
# For bool, support --key and --no-key with
|
|
691
|
+
# action=argparse.BooleanOptionalAction. Note, this means you cannot use
|
|
692
|
+
# --some-bool=True, or --some-bool=False, has to be --some-bool or
|
|
693
|
+
# --no-some-bool.
|
|
694
|
+
# Also we cannot have self.args.keys() that start with 'no-', which is
|
|
695
|
+
# why this entire thing is wrapped with try/except.
|
|
696
|
+
parser.add_argument(
|
|
697
|
+
*arguments, default=None, **bool_action_kwargs, **help_kwargs)
|
|
698
|
+
elif isinstance(value, (list, set)):
|
|
699
|
+
parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
|
|
700
|
+
elif isinstance(value, (int, str)):
|
|
701
|
+
parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
|
|
702
|
+
elif value is None:
|
|
703
|
+
parser.add_argument(*arguments, default=None, **help_kwargs)
|
|
704
|
+
else:
|
|
705
|
+
assert False, f'{key=} {value=} how do we do argparse for this type of value?'
|
|
706
|
+
except Exception as e:
|
|
707
|
+
if isinstance(value, bool):
|
|
708
|
+
self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
|
|
709
|
+
f'{arguments=} {bool_action_kwargs=} {help_kwargs=} {e=}')
|
|
710
|
+
else:
|
|
711
|
+
self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
|
|
712
|
+
f'{arguments=} {help_kwargs=} {e=}')
|
|
713
|
+
|
|
714
|
+
|
|
618
715
|
|
|
619
716
|
return parser
|
|
620
717
|
|
|
@@ -712,7 +809,7 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
712
809
|
def get_command_from_unparsed_args(
|
|
713
810
|
self, tokens: list, error_if_no_command: bool = True
|
|
714
811
|
) -> str:
|
|
715
|
-
'''Given a list of unparsed args, try to fish out the eda
|
|
812
|
+
'''Given a list of unparsed args, try to fish out the eda COMMAND value.
|
|
716
813
|
|
|
717
814
|
This will remove the value from the tokens list.
|
|
718
815
|
'''
|
|
@@ -724,7 +821,7 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
724
821
|
break
|
|
725
822
|
|
|
726
823
|
if not ret and error_if_no_command:
|
|
727
|
-
self.error(f"Looking for a valid eda {self.command_name}
|
|
824
|
+
self.error(f"Looking for a valid eda {self.command_name} COMMAND",
|
|
728
825
|
f"but didn't find one in {tokens=}",
|
|
729
826
|
error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
|
|
730
827
|
return ret
|
|
@@ -790,19 +887,16 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
790
887
|
if self.args_help has additional help information.
|
|
791
888
|
'''
|
|
792
889
|
|
|
793
|
-
# Indent long lines (>100) to indent=
|
|
794
|
-
def indent_me(text:str):
|
|
795
|
-
return indent_wrap_long_text(
|
|
890
|
+
# Indent long lines (>100) to indent=24 (this is where usual argparse help leaves off)
|
|
891
|
+
def indent_me(text: str, initial_indent: int = 23):
|
|
892
|
+
return indent_wrap_long_text(
|
|
893
|
+
' ' * initial_indent + text, width=get_terminal_columns(), indent=24
|
|
894
|
+
)
|
|
796
895
|
|
|
797
|
-
util.info('Help:')
|
|
896
|
+
util.info('Help:', color=Colors.cyan)
|
|
798
897
|
# using bare 'print' here, since help was requested, avoids --color and --quiet
|
|
799
898
|
print()
|
|
800
|
-
|
|
801
|
-
if no_targets:
|
|
802
|
-
print(f' eda [options] {self.command_name} [options]')
|
|
803
|
-
else:
|
|
804
|
-
print(f' eda [options] {self.command_name} [options] [files|targets, ...]')
|
|
805
|
-
print()
|
|
899
|
+
print_eda_usage_line(no_targets=no_targets, command_name=self.command_name)
|
|
806
900
|
|
|
807
901
|
print_base_help()
|
|
808
902
|
lines = []
|
|
@@ -811,10 +905,18 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
811
905
|
return
|
|
812
906
|
|
|
813
907
|
if self.command_name:
|
|
814
|
-
|
|
815
|
-
|
|
908
|
+
line = (
|
|
909
|
+
f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help for"
|
|
910
|
+
f" command={Colors.byellow}{self.command_name}{Colors.cyan}"
|
|
911
|
+
)
|
|
912
|
+
if tool := get_class_tool_name(self):
|
|
913
|
+
line += f" tool={Colors.byellow}{tool}"
|
|
914
|
+
line += f" {Colors.normal}(using '{self.__class__.__name__}')"
|
|
915
|
+
lines.append(line)
|
|
816
916
|
else:
|
|
817
|
-
lines.append(
|
|
917
|
+
lines.append(
|
|
918
|
+
f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help:{Colors.normal}"
|
|
919
|
+
)
|
|
818
920
|
|
|
819
921
|
# Attempt to run argparser on args, but don't error if it fails.
|
|
820
922
|
unparsed = []
|
|
@@ -824,44 +926,46 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
824
926
|
except Exception:
|
|
825
927
|
pass
|
|
826
928
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
vstr = "'" + v + "'"
|
|
841
|
-
lines.append(indent_me(f" --{k:20} : string : {vstr:12}{khelp}"))
|
|
842
|
-
else:
|
|
843
|
-
lines.append(indent_me(f" --{k:20} : <unknown> : {vstr:12}{khelp}"))
|
|
929
|
+
short_help_lines = util.get_argparser_short_help(
|
|
930
|
+
parser=self.get_argparser(support_underscores=False)
|
|
931
|
+
).split('\n')
|
|
932
|
+
short_help_lines.pop(0) # Strip first line w/ argparser prog name
|
|
933
|
+
lines.extend(short_help_lines)
|
|
934
|
+
|
|
935
|
+
# For these custom args, -GParameter=Value, +define+Name[=value] +incdir+path
|
|
936
|
+
# make colors to look like python >= 3.14, if that is our version.
|
|
937
|
+
color = Colors()
|
|
938
|
+
if sys.version_info.major < 3 or \
|
|
939
|
+
(sys.version_info.major == 3 and sys.version_info.minor < 14):
|
|
940
|
+
color.disable() # strip our color object if < 3.14
|
|
941
|
+
|
|
844
942
|
|
|
845
943
|
lines.append('')
|
|
944
|
+
lines.append(
|
|
945
|
+
f" {color.cyan}-G{color.byellow}<parameterName>{color.normal}=" \
|
|
946
|
+
+ f"{color.yellow}<value>{color.normal}"
|
|
947
|
+
)
|
|
846
948
|
lines.append(indent_me((
|
|
847
|
-
" -G<parameterName>=<value> "
|
|
848
949
|
" Add parameter to top level, support bit/int/string types only."
|
|
849
950
|
" Example: -GDEPTH=8 (DEPTH treated as SV int/integer)."
|
|
850
951
|
" -GENABLE=1 (ENABLED treated as SV bit/int/integer)."
|
|
851
952
|
" -GName=eda (Name treated as SV string \"eda\")."
|
|
852
953
|
)))
|
|
954
|
+
|
|
955
|
+
lines.append(f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}")
|
|
853
956
|
lines.append(indent_me((
|
|
854
|
-
" +define+<defineName> "
|
|
855
957
|
" Add define w/out value to tool ahead of SV sources"
|
|
856
958
|
" Example: +define+SIM_SPEEDUP"
|
|
857
959
|
)))
|
|
960
|
+
lines.append(
|
|
961
|
+
f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}=" \
|
|
962
|
+
+ f"{color.yellow}<value>{color.normal}")
|
|
858
963
|
lines.append(indent_me((
|
|
859
|
-
" +define+<defineName>=<value> "
|
|
860
964
|
" Add define w/ value to tool ahead of SV sources"
|
|
861
965
|
" Example: +define+TECH_LIB=2 +define+FULL_NAME=\"E D A\""
|
|
862
966
|
)))
|
|
967
|
+
lines.append(f" {color.cyan}+incdir+{color.byellow}PATH{color.normal}")
|
|
863
968
|
lines.append(indent_me((
|
|
864
|
-
" +incdir+<path> "
|
|
865
969
|
" Add path (absolute or relative) for include directories"
|
|
866
970
|
" for SystemVerilog `include \"<some-file>.svh\""
|
|
867
971
|
" Example: +incdir+../lib"
|
|
@@ -980,6 +1084,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
980
1084
|
self.files_vhd = []
|
|
981
1085
|
self.files_cpp = []
|
|
982
1086
|
self.files_sdc = []
|
|
1087
|
+
self.files_py = []
|
|
1088
|
+
self.files_makefile = []
|
|
983
1089
|
self.files_non_source = []
|
|
984
1090
|
self.files_caller_info = {}
|
|
985
1091
|
self.dep_shell_commands = [] # each list entry is a {}
|
|
@@ -1139,7 +1245,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1139
1245
|
self.files[new_key] = True
|
|
1140
1246
|
|
|
1141
1247
|
my_file_lists_list = [self.files_v, self.files_sv, self.files_vhd, self.files_cpp,
|
|
1142
|
-
self.files_sdc]
|
|
1248
|
+
self.files_sdc, self.files_py, self.files_makefile]
|
|
1143
1249
|
for my_file_list in my_file_lists_list:
|
|
1144
1250
|
for i,value in enumerate(my_file_list):
|
|
1145
1251
|
if value and isinstance(value, str) and \
|
|
@@ -1157,9 +1263,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1157
1263
|
need to be copied or linked to the work-dir. For example, if some SV assumes it
|
|
1158
1264
|
can $readmemh('file_that_is_here.txt') but we're running out of work-dir. Linking
|
|
1159
1265
|
is the easy work-around vs trying to run-in-place of all SV files.
|
|
1266
|
+
|
|
1267
|
+
Note that we also include .py and Makefile(s) in this.
|
|
1160
1268
|
'''
|
|
1161
1269
|
|
|
1162
|
-
for fname in self.files_non_source:
|
|
1270
|
+
for fname in self.files_non_source + self.files_py + self.files_makefile:
|
|
1163
1271
|
_, leaf_fname = os.path.split(fname)
|
|
1164
1272
|
destfile = os.path.join(self.args['work-dir'], leaf_fname)
|
|
1165
1273
|
relfname = os.path.relpath(fname)
|
|
@@ -1518,7 +1626,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1518
1626
|
# if we've found any target since being called, it means we found the one we were called for
|
|
1519
1627
|
return found_target
|
|
1520
1628
|
|
|
1521
|
-
def add_file( # pylint: disable=too-many-locals,too-many-branches
|
|
1629
|
+
def add_file( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
1522
1630
|
self, filename: str, use_abspath: bool = True, add_to_non_sources: bool = False,
|
|
1523
1631
|
caller_info: str = '', forced_extension: str = ''
|
|
1524
1632
|
) -> str:
|
|
@@ -1543,6 +1651,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1543
1651
|
cpp_file_ext_list = known_file_ext_dict.get('cpp', [])
|
|
1544
1652
|
sdc_file_ext_list = known_file_ext_dict.get('synth_constraints', [])
|
|
1545
1653
|
dotf_file_ext_list = known_file_ext_dict.get('dotf', [])
|
|
1654
|
+
py_file_ext_list = known_file_ext_dict.get('python', [])
|
|
1655
|
+
makefile_ext_list = known_file_ext_dict.get('makefile', [])
|
|
1546
1656
|
|
|
1547
1657
|
if forced_extension:
|
|
1548
1658
|
# If forced_extension='systemverilog', then use the first known extension for
|
|
@@ -1582,6 +1692,12 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1582
1692
|
caller_info=caller_info)
|
|
1583
1693
|
dp.apply_args(args_list=[f'-f={file_abspath}'])
|
|
1584
1694
|
del dp
|
|
1695
|
+
elif file_ext in py_file_ext_list:
|
|
1696
|
+
self.files_py.append(file_abspath)
|
|
1697
|
+
util.debug(f"Added Python file {filename} as {file_abspath}")
|
|
1698
|
+
elif file_ext in makefile_ext_list or os.path.split(filename)[1] == 'Makefile':
|
|
1699
|
+
self.files_makefile.append(file_abspath)
|
|
1700
|
+
util.debug(f"Added Makefile {filename} as {file_abspath}")
|
|
1585
1701
|
else:
|
|
1586
1702
|
# unknown file extension. In these cases we link the file to the working directory
|
|
1587
1703
|
# so it is available (for example, a .mem file that is expected to exist with relative
|
|
@@ -2114,21 +2230,22 @@ class CommandParallel(Command):
|
|
|
2114
2230
|
work_queue.put((jobs_launched, command_list, job['name'], cwd))
|
|
2115
2231
|
suffix = "<START>"
|
|
2116
2232
|
if fancy_mode:
|
|
2117
|
-
util.fancy_print(job_text+suffix, worker)
|
|
2233
|
+
util.fancy_print(job_text + suffix, worker)
|
|
2118
2234
|
elif failed_jobs:
|
|
2119
2235
|
# if we aren't in fancy mode, we will print a START line, periodic RUNNING
|
|
2120
2236
|
# lines, and PASS/FAIL line per-job
|
|
2121
|
-
util.
|
|
2237
|
+
util.print_yellow(job_text, end='')
|
|
2238
|
+
util.print_foreground_color(suffix)
|
|
2122
2239
|
else:
|
|
2123
|
-
util.
|
|
2240
|
+
util.print_foreground_color(job_text + suffix)
|
|
2124
2241
|
else:
|
|
2125
2242
|
# single-threaded job launch, we are going to print out job info as we start
|
|
2126
2243
|
# each job... no newline. since non-verbose silences the job and prints only
|
|
2127
2244
|
# <PASS>/<FAIL> after the trailing "..." we leave here
|
|
2128
2245
|
if failed_jobs:
|
|
2129
|
-
util.print_orange(job_text, end="")
|
|
2130
|
-
else:
|
|
2131
2246
|
util.print_yellow(job_text, end="")
|
|
2247
|
+
else:
|
|
2248
|
+
util.print_foreground_color(job_text, end="")
|
|
2132
2249
|
job_done_number = jobs_launched
|
|
2133
2250
|
job_done_name = job['name']
|
|
2134
2251
|
job_start_time = time.time()
|
|
@@ -2182,9 +2299,10 @@ class CommandParallel(Command):
|
|
|
2182
2299
|
if fancy_mode:
|
|
2183
2300
|
util.fancy_print(f"{job_text}{suffix}", t['worker'])
|
|
2184
2301
|
elif failed_jobs:
|
|
2185
|
-
util.
|
|
2302
|
+
util.print_yellow(job_text, end='')
|
|
2303
|
+
util.print_foreground_color(suffix)
|
|
2186
2304
|
else:
|
|
2187
|
-
util.
|
|
2305
|
+
util.print_foreground_color(job_text + suffix)
|
|
2188
2306
|
|
|
2189
2307
|
# shared job completion code
|
|
2190
2308
|
# single or multi-threaded, we can arrive here to harvest <= 1 jobs, and need
|
|
@@ -2192,36 +2310,52 @@ class CommandParallel(Command):
|
|
|
2192
2310
|
# printed, ready for pass/fail
|
|
2193
2311
|
if job_done:
|
|
2194
2312
|
jobs_complete += 1
|
|
2313
|
+
this_job_failed = False
|
|
2195
2314
|
if job_done_return_code is None or job_done_return_code:
|
|
2196
|
-
# embed the color code, to change color of pass/fail during the
|
|
2197
|
-
# util.print_orange/yellow below
|
|
2198
2315
|
if job_done_return_code == 124:
|
|
2199
2316
|
# bash uses 124 for bash timeout errors, if that was preprended to the
|
|
2200
2317
|
# command list.
|
|
2201
|
-
suffix =
|
|
2318
|
+
suffix = (
|
|
2319
|
+
f"<TOUT{safe_emoji(' ❌')}:"
|
|
2320
|
+
f" {sprint_time(job_done_run_time)}>"
|
|
2321
|
+
)
|
|
2322
|
+
this_job_failed = True
|
|
2202
2323
|
else:
|
|
2203
|
-
suffix =
|
|
2324
|
+
suffix = (
|
|
2325
|
+
f"<FAIL{safe_emoji(' ❌')}:"
|
|
2326
|
+
f" {sprint_time(job_done_run_time)}>"
|
|
2327
|
+
)
|
|
2328
|
+
this_job_failed = True
|
|
2204
2329
|
failed_jobs.append(job_done_name)
|
|
2205
2330
|
else:
|
|
2206
|
-
suffix =
|
|
2331
|
+
suffix = (
|
|
2332
|
+
f"<PASS{safe_emoji(' ✅')}:"
|
|
2333
|
+
f" {sprint_time(job_done_run_time)}>"
|
|
2334
|
+
)
|
|
2207
2335
|
passed_jobs.append(job_done_name)
|
|
2208
2336
|
# we want to print in one shot, because in fancy modes that's all that we're allowed
|
|
2209
2337
|
job_done_text = "" if job_done_quiet else sprint_job_line(job_done_number,
|
|
2210
2338
|
job_done_name)
|
|
2211
|
-
if
|
|
2212
|
-
util.
|
|
2213
|
-
|
|
2339
|
+
if this_job_failed:
|
|
2340
|
+
util.print_red(f"{job_done_text}{suffix}")
|
|
2341
|
+
elif failed_jobs:
|
|
2214
2342
|
util.print_yellow(f"{job_done_text}{suffix}")
|
|
2343
|
+
else:
|
|
2344
|
+
util.print_green(f"{job_done_text}{suffix}")
|
|
2215
2345
|
self.jobs_status[job_done_number-1] = job_done_return_code
|
|
2216
2346
|
|
|
2217
2347
|
if not anything_done:
|
|
2218
2348
|
time.sleep(0.25) # if nothing happens for an iteration, chill out a bit
|
|
2219
2349
|
|
|
2220
2350
|
if total_jobs:
|
|
2221
|
-
|
|
2222
|
-
|
|
2351
|
+
if len(passed_jobs) == total_jobs:
|
|
2352
|
+
emojitxt = safe_emoji('😀', ':)')
|
|
2353
|
+
else:
|
|
2354
|
+
emojitxt = safe_emoji('😦', ':(')
|
|
2355
|
+
util.info(sprint_job_line(final=True, job_name="jobs passed") + f"< {emojitxt} >",
|
|
2356
|
+
start="")
|
|
2223
2357
|
else:
|
|
2224
|
-
util.info("Parallel: <No jobs found>")
|
|
2358
|
+
util.info(f"Parallel: <{safe_emoji('❓ ')}No jobs found>")
|
|
2225
2359
|
# Make sure all jobs have a set status:
|
|
2226
2360
|
for i, rc in enumerate(self.jobs_status):
|
|
2227
2361
|
if rc is None or not isinstance(rc, int):
|
opencos/eda_config.py
CHANGED
|
@@ -15,6 +15,7 @@ import shutil
|
|
|
15
15
|
import mergedeep
|
|
16
16
|
|
|
17
17
|
from opencos import util
|
|
18
|
+
from opencos.util import safe_emoji
|
|
18
19
|
from opencos.utils.markup_helpers import yaml_safe_load, yaml_safe_writer
|
|
19
20
|
|
|
20
21
|
class Defaults:
|
|
@@ -231,7 +232,7 @@ def get_config_merged_with_defaults(config:dict) -> dict:
|
|
|
231
232
|
def get_argparser() -> argparse.ArgumentParser:
|
|
232
233
|
'''Returns an ArgumentParser, handles --config-yml=<filename> arg'''
|
|
233
234
|
parser = argparse.ArgumentParser(
|
|
234
|
-
prog='opencos eda config options', add_help=False, allow_abbrev=False
|
|
235
|
+
prog=f'{safe_emoji("🔎 ")}opencos eda config options', add_help=False, allow_abbrev=False
|
|
235
236
|
)
|
|
236
237
|
parser.add_argument('--config-yml', type=str, default=Defaults.config_yml,
|
|
237
238
|
help=('YAML filename to use for configuration (default'
|
opencos/eda_config_defaults.yml
CHANGED
|
@@ -94,6 +94,11 @@ file_extensions:
|
|
|
94
94
|
dotf:
|
|
95
95
|
- .f
|
|
96
96
|
- .vc
|
|
97
|
+
python:
|
|
98
|
+
- .py
|
|
99
|
+
makefile:
|
|
100
|
+
- .mk
|
|
101
|
+
|
|
97
102
|
|
|
98
103
|
inferred_top:
|
|
99
104
|
# file extensions that we can infer "top" module from, if --top omitted.
|
|
@@ -332,7 +337,6 @@ tools:
|
|
|
332
337
|
- "COCOTB_TEST_FAILED"
|
|
333
338
|
log-must-strings:
|
|
334
339
|
- "passed"
|
|
335
|
-
- "Cocotb test completed successfully!"
|
|
336
340
|
|
|
337
341
|
|
|
338
342
|
quartus:
|