opencos-eda 0.3.3__py3-none-any.whl → 0.3.6__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/sim.py +16 -6
- opencos/deps/defaults.py +1 -0
- opencos/deps/deps_processor.py +60 -24
- opencos/deps_schema.py +17 -0
- opencos/eda.py +13 -19
- opencos/eda_base.py +78 -29
- opencos/eda_config.py +86 -41
- opencos/eda_config_defaults.yml +8 -1
- opencos/eda_extract_targets.py +0 -0
- opencos/eda_tool_helper.py +19 -0
- opencos/export_helper.py +89 -31
- opencos/files.py +3 -1
- opencos/hw/oc_cli.py +0 -0
- opencos/tests/helpers.py +60 -17
- opencos/tests/test_eda.py +11 -1
- opencos/tools/cocotb.py +94 -21
- opencos/tools/iverilog.py +4 -0
- opencos/tools/riviera.py +23 -12
- opencos/tools/verilator.py +91 -2
- opencos/tools/vivado.py +1 -0
- opencos/util.py +86 -57
- opencos/utils/str_helpers.py +6 -1
- opencos/utils/subprocess_helpers.py +66 -3
- {opencos_eda-0.3.3.dist-info → opencos_eda-0.3.6.dist-info}/METADATA +12 -3
- {opencos_eda-0.3.3.dist-info → opencos_eda-0.3.6.dist-info}/RECORD +28 -28
- {opencos_eda-0.3.3.dist-info → opencos_eda-0.3.6.dist-info}/WHEEL +0 -0
- {opencos_eda-0.3.3.dist-info → opencos_eda-0.3.6.dist-info}/entry_points.txt +0 -0
- {opencos_eda-0.3.3.dist-info → opencos_eda-0.3.6.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.3.3.dist-info → opencos_eda-0.3.6.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.3.3.dist-info → opencos_eda-0.3.6.dist-info}/top_level.txt +0 -0
opencos/commands/sim.py
CHANGED
|
@@ -145,6 +145,7 @@ class CommandSim(CommandDesign):
|
|
|
145
145
|
self.run_dep_commands()
|
|
146
146
|
self.do_it()
|
|
147
147
|
self.run_post_tool_dep_commands()
|
|
148
|
+
self.report_pass_fail()
|
|
148
149
|
return unparsed
|
|
149
150
|
|
|
150
151
|
|
|
@@ -198,6 +199,9 @@ class CommandSim(CommandDesign):
|
|
|
198
199
|
|
|
199
200
|
clist = list(obj).copy()
|
|
200
201
|
tee_fpath = getattr(obj, 'tee_fpath', None)
|
|
202
|
+
work_dir = getattr(obj, 'work_dir', None)
|
|
203
|
+
if not work_dir:
|
|
204
|
+
work_dir = self.args['work-dir']
|
|
201
205
|
|
|
202
206
|
util.debug(f'run_commands_check_logs: {clist=}, {tee_fpath=}')
|
|
203
207
|
|
|
@@ -207,24 +211,28 @@ class CommandSim(CommandDesign):
|
|
|
207
211
|
if log_filename:
|
|
208
212
|
log_fname = log_filename
|
|
209
213
|
|
|
214
|
+
|
|
210
215
|
_, stdout, _ = self.exec(
|
|
211
|
-
work_dir=
|
|
216
|
+
work_dir=work_dir, command_list=clist, tee_fpath=tee_fpath
|
|
212
217
|
)
|
|
213
218
|
|
|
214
219
|
if check_logs and log_fname:
|
|
215
220
|
# Note this call will check on stdout if not GUI, not opening the log_fname,
|
|
216
221
|
# but if this is GUI we normally lose stdout and have to open the log.
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
if self.args.get('gui', False):
|
|
223
|
+
file_contents_str = ''
|
|
224
|
+
else:
|
|
225
|
+
file_contents_str = stdout
|
|
226
|
+
|
|
219
227
|
self.check_logs_for_errors(
|
|
220
|
-
filename=os.path.join(
|
|
228
|
+
filename=os.path.join(work_dir, log_fname),
|
|
221
229
|
file_contents_str=file_contents_str,
|
|
222
230
|
bad_strings=bad_strings, must_strings=must_strings,
|
|
223
231
|
use_bad_strings=use_bad_strings, use_must_strings=use_must_strings
|
|
224
232
|
)
|
|
225
233
|
if log_fname:
|
|
226
234
|
self.artifacts_add(
|
|
227
|
-
name=os.path.join(
|
|
235
|
+
name=os.path.join(work_dir, log_fname),
|
|
228
236
|
typ='text', description='Simulator stdout/stderr log file'
|
|
229
237
|
)
|
|
230
238
|
|
|
@@ -253,6 +261,7 @@ class CommandSim(CommandDesign):
|
|
|
253
261
|
tool = self.args.get('tool', None)
|
|
254
262
|
# Certain args are allow-listed here
|
|
255
263
|
deps_file_args = []
|
|
264
|
+
print(f'SUPER DREW DEBUG: {self.get_command_line_args()=}')
|
|
256
265
|
for a in self.get_command_line_args():
|
|
257
266
|
if any(a.startswith(x) for x in [
|
|
258
267
|
'--compile-args',
|
|
@@ -265,7 +274,8 @@ class CommandSim(CommandDesign):
|
|
|
265
274
|
'--stop-',
|
|
266
275
|
'--lint-',
|
|
267
276
|
'--verilate',
|
|
268
|
-
'--verilator'
|
|
277
|
+
'--verilator',
|
|
278
|
+
'--cocotb-test-']):
|
|
269
279
|
deps_file_args.append(a)
|
|
270
280
|
|
|
271
281
|
export_obj.run(
|
opencos/deps/defaults.py
CHANGED
opencos/deps/deps_processor.py
CHANGED
|
@@ -10,7 +10,7 @@ import os
|
|
|
10
10
|
from opencos import files
|
|
11
11
|
from opencos import eda_config
|
|
12
12
|
from opencos.util import debug, info, warning, error, read_tokens_from_dot_f, \
|
|
13
|
-
patch_args_for_dir
|
|
13
|
+
patch_args_for_dir, load_env_file
|
|
14
14
|
from opencos.utils.str_helpers import dep_str2list
|
|
15
15
|
from opencos.deps.deps_file import deps_target_get_deps_list
|
|
16
16
|
from opencos.deps.deps_commands import deps_commands_handler
|
|
@@ -77,7 +77,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
77
77
|
).get('command_handler', {}).keys()
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def apply_defines(self, defines_dict: dict):
|
|
80
|
+
def apply_defines(self, defines_dict: dict) -> None:
|
|
81
81
|
'''Given defines_dict, applies them to our self.command_design_ref obj'''
|
|
82
82
|
if not isinstance(defines_dict, dict):
|
|
83
83
|
self.error(f"{defines_dict=} is not type dict, can't apply defines,",
|
|
@@ -95,7 +95,19 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
95
95
|
self.command_design_ref.process_plusarg(f'+define+{k}={v}')
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def apply_plusargs(self, plusargs_dict: dict) -> None:
|
|
99
|
+
'''Given plusarsg_dict, applies them to our self.command_design_ref obj'''
|
|
100
|
+
if not isinstance(plusargs_dict, dict):
|
|
101
|
+
self.error(f"{plusargs_dict=} is not type dict, can't apply plusargs,",
|
|
102
|
+
f"in {self.caller_info}")
|
|
103
|
+
for k,v in plusargs_dict.items():
|
|
104
|
+
if v is None or v == '':
|
|
105
|
+
self.command_design_ref.process_plusarg(f'+{k}')
|
|
106
|
+
else:
|
|
107
|
+
self.command_design_ref.process_plusarg(f'+{k}={v}')
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def apply_parameters(self, parameters_dict: dict) -> None:
|
|
99
111
|
'''Given parameters_dict, applies them to our self.command_design_ref obj'''
|
|
100
112
|
if not isinstance(parameters_dict, dict):
|
|
101
113
|
self.error(f"{parameters_dict=} is not type dict, can't apply defines,",
|
|
@@ -110,7 +122,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
110
122
|
)
|
|
111
123
|
|
|
112
124
|
|
|
113
|
-
def apply_incdirs(self, incdirs_list:list):
|
|
125
|
+
def apply_incdirs(self, incdirs_list:list) -> None:
|
|
114
126
|
'''Given incdirs_list, applies them to our self.command_design_ref obj'''
|
|
115
127
|
if not isinstance(incdirs_list, (str, list)):
|
|
116
128
|
self.error(f"{incdirs_list=} is not type str/list, can't apply incdirs",
|
|
@@ -180,9 +192,15 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
180
192
|
# Since some args (util.py, eda_config.py, eda.py) can only be handled from command
|
|
181
193
|
# line, it would be nice if -f or --input-file is handled from DEPS, so we'll special
|
|
182
194
|
# case that now. Recursively resolve -f / --input-file.
|
|
195
|
+
# Do similary for --env-file (also only supported in util.py)
|
|
183
196
|
parser = argparse.ArgumentParser(
|
|
184
197
|
prog='deps_processor -f/--input-file', add_help=False, allow_abbrev=False
|
|
185
198
|
)
|
|
199
|
+
parser.add_argument('--env-file', default=[], action='append',
|
|
200
|
+
help=(
|
|
201
|
+
"dotenv file(s) to pass ENV vars, (default: .env loaded first,"
|
|
202
|
+
" subsequent files' vars override .env"
|
|
203
|
+
))
|
|
186
204
|
parser.add_argument('-f', '--input-file', default=[], action='append',
|
|
187
205
|
help=(
|
|
188
206
|
'Input .f file to be expanded as eda args, defines, incdirs,'
|
|
@@ -213,7 +231,12 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
213
231
|
# recurse until we've resolved nested .f files.
|
|
214
232
|
return self.apply_args(args_list=tokens2)
|
|
215
233
|
|
|
216
|
-
|
|
234
|
+
if parsed.env_file:
|
|
235
|
+
for env_file in parsed.env_file:
|
|
236
|
+
load_env_file(env_file)
|
|
237
|
+
|
|
238
|
+
# if no --input-file/--env-file values, keep parsing the remaining tokens2:
|
|
239
|
+
tokens = tokens2
|
|
217
240
|
|
|
218
241
|
# We have to special-case anything with --tool[=value] in tokens, otherwise
|
|
219
242
|
# the user may think they were allowed to set --tool, but in our flow the Command handler
|
|
@@ -290,7 +313,9 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
290
313
|
caller_info=self.caller_info
|
|
291
314
|
)
|
|
292
315
|
|
|
293
|
-
def process_deps_entry(
|
|
316
|
+
def process_deps_entry( # pylint: disable=too-many-branches
|
|
317
|
+
self
|
|
318
|
+
) -> list:
|
|
294
319
|
'''Main entry point (after creating DepsProcessor obj) to resolve a deps target
|
|
295
320
|
|
|
296
321
|
Example usage:
|
|
@@ -303,22 +328,6 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
303
328
|
This method will apply all target features to the CommandDesign ref object as
|
|
304
329
|
we traverse.
|
|
305
330
|
|
|
306
|
-
Supported target keys:
|
|
307
|
-
-- tags (or equivalent, to support multiple define/incdir/deps for a target)
|
|
308
|
-
-- supports tag-name, with-tools, with-args, args, defines, incdirs, deps
|
|
309
|
-
** to be applied if a tool matches.
|
|
310
|
-
-- TODO(drew): other features in docs/DEPS.md not yet implemented.
|
|
311
|
-
-- multi: ignore-this-target: - commands (handled in eda.py CommandMulti.resolve_target)
|
|
312
|
-
-- Named eda commands
|
|
313
|
-
-- (partially done) sim or other eda commands (eda.py command specific things)
|
|
314
|
-
basically, check the command, and apply/merge values to 'entry'?
|
|
315
|
-
-- args
|
|
316
|
-
-- defines
|
|
317
|
-
-- incdirs
|
|
318
|
-
-- top.
|
|
319
|
-
-- commands (not in deps)
|
|
320
|
-
-- deps
|
|
321
|
-
|
|
322
331
|
TODO(drew): This does not yet support conditional inclusions based on defines,
|
|
323
332
|
like the old DEPS files did with pattern:
|
|
324
333
|
SOME_DEFINE ? dep_if_define_present : dep_if_define_not_present
|
|
@@ -344,6 +353,8 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
344
353
|
remaining_deps_list += self.process_tags()
|
|
345
354
|
elif key == 'defines':
|
|
346
355
|
self.process_defines()
|
|
356
|
+
elif key == 'plusargs':
|
|
357
|
+
self.process_plusargs()
|
|
347
358
|
elif key == 'parameters':
|
|
348
359
|
self.process_parameters()
|
|
349
360
|
elif key == 'incdirs':
|
|
@@ -520,6 +531,10 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
520
531
|
# apply defines:
|
|
521
532
|
self.apply_defines(value.get('defines', {}))
|
|
522
533
|
|
|
534
|
+
if key == 'plusargs':
|
|
535
|
+
# apply plusargs:
|
|
536
|
+
self.apply_plusargs(value.get('plusargs', {}))
|
|
537
|
+
|
|
523
538
|
elif key == 'parameters':
|
|
524
539
|
self.apply_parameters(value.get('parameters', {}))
|
|
525
540
|
|
|
@@ -598,7 +613,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
598
613
|
return ret_deps_added_from_tags
|
|
599
614
|
|
|
600
615
|
|
|
601
|
-
def process_defines(self):
|
|
616
|
+
def process_defines(self) -> None:
|
|
602
617
|
'''Returns None, applies defines (dict, if any) from self.deps_entry to
|
|
603
618
|
self.command_design_ref.'''
|
|
604
619
|
|
|
@@ -614,7 +629,28 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
614
629
|
|
|
615
630
|
self.apply_defines(entry_defines)
|
|
616
631
|
|
|
617
|
-
|
|
632
|
+
|
|
633
|
+
def process_plusargs(self) -> None:
|
|
634
|
+
'''Returns None, applies plusargs (dict, if any) from self.deps_entry to
|
|
635
|
+
self.command_design_ref.
|
|
636
|
+
|
|
637
|
+
These work w/ the same rules as defines (no value, or value int/str)
|
|
638
|
+
'''
|
|
639
|
+
|
|
640
|
+
# Plusargs:
|
|
641
|
+
# apply command specific plusargs, with higher priority than the a
|
|
642
|
+
# deps_entry['sim']['plusargs'] entry,
|
|
643
|
+
# do this with dict1.update(dict2):
|
|
644
|
+
entry_plusargs = {}
|
|
645
|
+
entry_plusargs.update(self.deps_entry.get('plusargs', {}))
|
|
646
|
+
entry_plusargs.update(self.entry_eda_command.get('plusargs', {}))
|
|
647
|
+
assert isinstance(entry_plusargs, dict), \
|
|
648
|
+
f'{entry_plusargs=} for in {self.caller_info} must be a dict'
|
|
649
|
+
|
|
650
|
+
self.apply_plusargs(entry_plusargs)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def process_parameters(self) -> None:
|
|
618
654
|
'''Returns None, applies parameters (dict, if any) from self.deps_entry to
|
|
619
655
|
self.command_design_ref.'''
|
|
620
656
|
|
opencos/deps_schema.py
CHANGED
|
@@ -17,6 +17,9 @@ my_target_name:
|
|
|
17
17
|
incdirs: <---- incdirs, optional array (or string)
|
|
18
18
|
- ./
|
|
19
19
|
|
|
20
|
+
plusargs: <---- plusargs, optional table
|
|
21
|
+
some_plusarg: 32
|
|
22
|
+
|
|
20
23
|
top: tb <---- top, optional string
|
|
21
24
|
|
|
22
25
|
deps:
|
|
@@ -63,6 +66,7 @@ my_target_name:
|
|
|
63
66
|
defines: <---- defines, optional table
|
|
64
67
|
parameters: <---- parameters, optional table
|
|
65
68
|
incdirs: <---- incdirs, optional array (or string)
|
|
69
|
+
plusargs: <---- plusargs, optional table
|
|
66
70
|
top: tb <---- top, optional string
|
|
67
71
|
deps: <---- TARGET_DEPS_CONTENTS schema
|
|
68
72
|
- some_file.sv <---- string file name
|
|
@@ -216,6 +220,9 @@ TARGET_EDA_COMMAND_ENTRY_TABLE = {
|
|
|
216
220
|
Optional('defines'): {
|
|
217
221
|
Optional(str): Or(str, int, type(None)),
|
|
218
222
|
},
|
|
223
|
+
Optional('plusargs'): {
|
|
224
|
+
Optional(str): Or(str, int, type(None)),
|
|
225
|
+
},
|
|
219
226
|
Optional('parameters'): {
|
|
220
227
|
Optional(str): Or(str, int),
|
|
221
228
|
},
|
|
@@ -241,6 +248,9 @@ TARGET_TAGS_TABLE = {
|
|
|
241
248
|
Optional('defines'): {
|
|
242
249
|
Optional(str): Or(str, int, type(None)),
|
|
243
250
|
},
|
|
251
|
+
Optional('plusargs'): {
|
|
252
|
+
Optional(str): Or(str, int, type(None)),
|
|
253
|
+
},
|
|
244
254
|
Optional('parameters'): {
|
|
245
255
|
Optional(str): Or(str, int),
|
|
246
256
|
},
|
|
@@ -262,6 +272,10 @@ TARGET_CONTENTS = Or(
|
|
|
262
272
|
Optional('defines'): {
|
|
263
273
|
Optional(str): Or(str, int, type(None)),
|
|
264
274
|
},
|
|
275
|
+
# plusargs: table of key-value; value null or string
|
|
276
|
+
Optional('plusargs'): {
|
|
277
|
+
Optional(str): Or(str, int, type(None)),
|
|
278
|
+
},
|
|
265
279
|
# parameters: table of key-value
|
|
266
280
|
Optional('parameters'): {
|
|
267
281
|
Optional(str): Or(str, int),
|
|
@@ -324,6 +338,9 @@ FILE_SIMPLIFIED = Schema(
|
|
|
324
338
|
Optional('defines'): {
|
|
325
339
|
Optional(str): Or(type(None), str),
|
|
326
340
|
},
|
|
341
|
+
Optional('plusargs'): {
|
|
342
|
+
Optional(str): Or(type(None), str),
|
|
343
|
+
},
|
|
327
344
|
Optional('parameters'): {
|
|
328
345
|
Optional(str): str,
|
|
329
346
|
},
|
opencos/eda.py
CHANGED
|
@@ -20,10 +20,11 @@ from pathlib import Path
|
|
|
20
20
|
|
|
21
21
|
import opencos
|
|
22
22
|
from opencos import util, eda_config, eda_base
|
|
23
|
+
from opencos.util import safe_emoji
|
|
23
24
|
from opencos.eda_base import Tool, which_tool, get_eda_exec
|
|
24
25
|
from opencos.utils import vsim_helper, vscode_helper
|
|
25
26
|
from opencos.utils.subprocess_helpers import subprocess_run_background
|
|
26
|
-
from opencos.utils import status_constants, str_helpers
|
|
27
|
+
from opencos.utils import status_constants, str_helpers, subprocess_helpers
|
|
27
28
|
|
|
28
29
|
# Configure util:
|
|
29
30
|
util.progname = "EDA"
|
|
@@ -50,7 +51,7 @@ def init_config(
|
|
|
50
51
|
# For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
|
|
51
52
|
# the actual class using importlib (via opencos.util)
|
|
52
53
|
|
|
53
|
-
eda_config.
|
|
54
|
+
eda_config.update_config_auto_tool_order_for_tool(tool=tool, config=config)
|
|
54
55
|
|
|
55
56
|
config['command_handler'] = {}
|
|
56
57
|
for _cmd, str_class in config['DEFAULT_HANDLERS'].items():
|
|
@@ -95,15 +96,15 @@ def usage(tokens: list, config: dict, command: str = "", tool: str = "") -> int:
|
|
|
95
96
|
|
|
96
97
|
if command == "":
|
|
97
98
|
print(
|
|
98
|
-
"""
|
|
99
|
-
Usage:
|
|
99
|
+
f"""
|
|
100
|
+
{safe_emoji("🔦 ")}Usage:
|
|
100
101
|
eda [<options>] <command> [options] <files|targets, ...>
|
|
101
102
|
"""
|
|
102
103
|
)
|
|
103
104
|
print(get_all_commands_help_str(config=config))
|
|
104
105
|
print(
|
|
105
|
-
"""
|
|
106
|
-
|
|
106
|
+
f"""
|
|
107
|
+
{safe_emoji("❕ ")}where <files|targets, ...> is one or more source file or DEPS markup file target,
|
|
107
108
|
such as .v, .sv, .vhd[l], .cpp files, or a target key in a DEPS.[yml|yaml|toml|json].
|
|
108
109
|
Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` to
|
|
109
110
|
force use that file as systemverilog, verilog, vhdl, or C++, respectively.
|
|
@@ -163,10 +164,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
|
|
|
163
164
|
If so, updates config['auto_tools_order'][tool]['exe']
|
|
164
165
|
'''
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
tool = eda_config.update_config_auto_tool_order_for_tool(
|
|
168
|
-
tool=tool, config=config
|
|
169
|
-
)
|
|
167
|
+
tool = eda_config.tool_arg_remove_path_information(tool)
|
|
170
168
|
|
|
171
169
|
assert 'auto_tools_order' in config
|
|
172
170
|
assert isinstance(config['auto_tools_order'], list)
|
|
@@ -275,9 +273,7 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
|
|
|
275
273
|
|
|
276
274
|
'''
|
|
277
275
|
|
|
278
|
-
tool = eda_config.
|
|
279
|
-
tool=tool, config=config
|
|
280
|
-
)
|
|
276
|
+
tool = eda_config.tool_arg_remove_path_information(tool)
|
|
281
277
|
|
|
282
278
|
if not quiet and not auto_setup:
|
|
283
279
|
util.info(f"Setup for tool: '{tool}'")
|
|
@@ -389,6 +385,8 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
|
|
|
389
385
|
if not is_interactive:
|
|
390
386
|
# Run init_config() now, we deferred it in main(), but only run it
|
|
391
387
|
# for this tool (or tool=None to figure it out)
|
|
388
|
+
# This will handle any --tool=<name>=/path/to/bin also, so don't have to
|
|
389
|
+
# run tool_setup(..) on its own.
|
|
392
390
|
config = init_config(
|
|
393
391
|
config, tool=parsed.tool,
|
|
394
392
|
run_auto_tool_setup=run_auto_tool_setup
|
|
@@ -406,13 +404,8 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
|
|
|
406
404
|
for arg in unparsed:
|
|
407
405
|
if not arg.startswith('-'):
|
|
408
406
|
command = arg
|
|
409
|
-
if parsed.tool:
|
|
410
|
-
tool_setup(parsed.tool, config=config)
|
|
411
407
|
return usage(tokens=unparsed, config=config, command=command, tool=parsed.tool)
|
|
412
408
|
|
|
413
|
-
if parsed.tool:
|
|
414
|
-
tool_setup(parsed.tool, config=config)
|
|
415
|
-
|
|
416
409
|
deferred_tokens = unparsed
|
|
417
410
|
if not command:
|
|
418
411
|
util.error("Didn't get a command!")
|
|
@@ -503,7 +496,8 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
|
|
|
503
496
|
def signal_handler(sig, frame) -> None: # pylint: disable=unused-argument
|
|
504
497
|
'''Handles Ctrl-C, called by main_cli() if running from command line'''
|
|
505
498
|
util.fancy_stop()
|
|
506
|
-
util.
|
|
499
|
+
util.error(f'{safe_emoji("❌ ")}Received Ctrl+C...', start='\nINFO: [EDA] ')
|
|
500
|
+
subprocess_helpers.cleanup_all()
|
|
507
501
|
util.exit(-1)
|
|
508
502
|
|
|
509
503
|
# **************************************************************
|
opencos/eda_base.py
CHANGED
|
@@ -24,7 +24,7 @@ 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
29
|
indent_wrap_long_text, pretty_list_columns_manual
|
|
30
30
|
from opencos.utils.subprocess_helpers import subprocess_run_background
|
|
@@ -44,7 +44,9 @@ def print_base_help() -> None:
|
|
|
44
44
|
|
|
45
45
|
def get_argparser() -> argparse.ArgumentParser:
|
|
46
46
|
'''Returns the ArgumentParser for general eda CLI'''
|
|
47
|
-
parser = argparse.ArgumentParser(
|
|
47
|
+
parser = argparse.ArgumentParser(
|
|
48
|
+
prog=f'{safe_emoji("🔎 ")}eda options', add_help=False, allow_abbrev=False
|
|
49
|
+
)
|
|
48
50
|
parser.add_argument('-q', '--quit', action='store_true',
|
|
49
51
|
help=(
|
|
50
52
|
'For interactive mode (eda called with no options, command, or'
|
|
@@ -313,6 +315,18 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
313
315
|
util.error(f"command '{self.command_name}' has previous errors")
|
|
314
316
|
return self.status > 0
|
|
315
317
|
|
|
318
|
+
def report_pass_fail(self) -> None:
|
|
319
|
+
'''Reports an INFO line with pass/fail information'''
|
|
320
|
+
job_name = ' - '.join(
|
|
321
|
+
x for x in (self.command_name, self.args.get('tool', ''),
|
|
322
|
+
self.args.get('top', '')) if x
|
|
323
|
+
)
|
|
324
|
+
if self.status_any_error():
|
|
325
|
+
util.info(f'{safe_emoji("❌ ")}{job_name}: Errors observed.', color=Colors.red)
|
|
326
|
+
else:
|
|
327
|
+
util.info(f'{safe_emoji("✅ ")}{job_name}: No errors observed.')
|
|
328
|
+
|
|
329
|
+
|
|
316
330
|
def which_tool(self, command:str) -> str:
|
|
317
331
|
'''Returns a str for the tool name used for the requested command'''
|
|
318
332
|
return which_tool(command, config=self.config)
|
|
@@ -462,7 +476,8 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
462
476
|
if not tee_fpath and getattr(command_list, 'tee_fpath', None):
|
|
463
477
|
tee_fpath = getattr(command_list, 'tee_fpath', '')
|
|
464
478
|
if not quiet:
|
|
465
|
-
util.info(f"exec: {' '.join(command_list)}
|
|
479
|
+
util.info(f"{safe_emoji('⏩ ')}exec: {' '.join(command_list)}",
|
|
480
|
+
f"(in {work_dir}, {tee_fpath=})")
|
|
466
481
|
|
|
467
482
|
stdout, stderr, return_code = subprocess_run_background(
|
|
468
483
|
work_dir=work_dir,
|
|
@@ -484,7 +499,8 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
484
499
|
self.error(f"exec: returned with error (return code: {return_code})",
|
|
485
500
|
error_code=self.status)
|
|
486
501
|
else:
|
|
487
|
-
util.debug(f"exec: returned with error (return code:
|
|
502
|
+
util.debug(f"{safe_emoji('❌ ')}exec: returned with error (return code:",
|
|
503
|
+
f"{return_code})")
|
|
488
504
|
else:
|
|
489
505
|
util.debug(f"exec: returned without error (return code: {return_code})")
|
|
490
506
|
return stderr, stdout, return_code
|
|
@@ -564,7 +580,9 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
564
580
|
# parsed.args-with-dashes is not legal python. Some of self.args.keys() still have - or _,
|
|
565
581
|
# so this will handle both.
|
|
566
582
|
# Also, preference is for self.args.keys(), to be str with - dashes
|
|
567
|
-
parser = argparse.ArgumentParser(
|
|
583
|
+
parser = argparse.ArgumentParser(
|
|
584
|
+
prog=f'{safe_emoji("🔎 ")}eda', add_help=False, allow_abbrev=False
|
|
585
|
+
)
|
|
568
586
|
bool_action_kwargs = util.get_argparse_bool_action_kwargs()
|
|
569
587
|
|
|
570
588
|
if not parser_arg_list:
|
|
@@ -594,6 +612,7 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
594
612
|
help_kwargs = {'help': f'default={value}'}
|
|
595
613
|
else:
|
|
596
614
|
help_kwargs = {'help': f'{type(value).__name__} default={value}'}
|
|
615
|
+
help_kwargs['help'] = help_kwargs['help'].replace('%', '%%')
|
|
597
616
|
|
|
598
617
|
|
|
599
618
|
# It's important to set the default=None on these, except for list types where default
|
|
@@ -796,7 +815,7 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
796
815
|
util.info('Help:')
|
|
797
816
|
# using bare 'print' here, since help was requested, avoids --color and --quiet
|
|
798
817
|
print()
|
|
799
|
-
print('Usage:')
|
|
818
|
+
print(f'{safe_emoji("🔦 ")}Usage:')
|
|
800
819
|
if no_targets:
|
|
801
820
|
print(f' eda [options] {self.command_name} [options]')
|
|
802
821
|
else:
|
|
@@ -810,10 +829,10 @@ class Command: # pylint: disable=too-many-public-methods
|
|
|
810
829
|
return
|
|
811
830
|
|
|
812
831
|
if self.command_name:
|
|
813
|
-
lines.append(f"Generic help for command='{self.command_name}'"
|
|
832
|
+
lines.append(f"{safe_emoji('🔧 ')}Generic help for command='{self.command_name}'"
|
|
814
833
|
f" (using '{self.__class__.__name__}')")
|
|
815
834
|
else:
|
|
816
|
-
lines.append("Generic help (from class Command):")
|
|
835
|
+
lines.append("{safe_emoji('🔧 ')}Generic help (from class Command):")
|
|
817
836
|
|
|
818
837
|
# Attempt to run argparser on args, but don't error if it fails.
|
|
819
838
|
unparsed = []
|
|
@@ -979,6 +998,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
979
998
|
self.files_vhd = []
|
|
980
999
|
self.files_cpp = []
|
|
981
1000
|
self.files_sdc = []
|
|
1001
|
+
self.files_py = []
|
|
1002
|
+
self.files_makefile = []
|
|
982
1003
|
self.files_non_source = []
|
|
983
1004
|
self.files_caller_info = {}
|
|
984
1005
|
self.dep_shell_commands = [] # each list entry is a {}
|
|
@@ -1138,7 +1159,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1138
1159
|
self.files[new_key] = True
|
|
1139
1160
|
|
|
1140
1161
|
my_file_lists_list = [self.files_v, self.files_sv, self.files_vhd, self.files_cpp,
|
|
1141
|
-
self.files_sdc]
|
|
1162
|
+
self.files_sdc, self.files_py, self.files_makefile]
|
|
1142
1163
|
for my_file_list in my_file_lists_list:
|
|
1143
1164
|
for i,value in enumerate(my_file_list):
|
|
1144
1165
|
if value and isinstance(value, str) and \
|
|
@@ -1156,9 +1177,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1156
1177
|
need to be copied or linked to the work-dir. For example, if some SV assumes it
|
|
1157
1178
|
can $readmemh('file_that_is_here.txt') but we're running out of work-dir. Linking
|
|
1158
1179
|
is the easy work-around vs trying to run-in-place of all SV files.
|
|
1180
|
+
|
|
1181
|
+
Note that we also include .py and Makefile(s) in this.
|
|
1159
1182
|
'''
|
|
1160
1183
|
|
|
1161
|
-
for fname in self.files_non_source:
|
|
1184
|
+
for fname in self.files_non_source + self.files_py + self.files_makefile:
|
|
1162
1185
|
_, leaf_fname = os.path.split(fname)
|
|
1163
1186
|
destfile = os.path.join(self.args['work-dir'], leaf_fname)
|
|
1164
1187
|
relfname = os.path.relpath(fname)
|
|
@@ -1517,7 +1540,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1517
1540
|
# if we've found any target since being called, it means we found the one we were called for
|
|
1518
1541
|
return found_target
|
|
1519
1542
|
|
|
1520
|
-
def add_file( # pylint: disable=too-many-locals,too-many-branches
|
|
1543
|
+
def add_file( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
1521
1544
|
self, filename: str, use_abspath: bool = True, add_to_non_sources: bool = False,
|
|
1522
1545
|
caller_info: str = '', forced_extension: str = ''
|
|
1523
1546
|
) -> str:
|
|
@@ -1542,6 +1565,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1542
1565
|
cpp_file_ext_list = known_file_ext_dict.get('cpp', [])
|
|
1543
1566
|
sdc_file_ext_list = known_file_ext_dict.get('synth_constraints', [])
|
|
1544
1567
|
dotf_file_ext_list = known_file_ext_dict.get('dotf', [])
|
|
1568
|
+
py_file_ext_list = known_file_ext_dict.get('python', [])
|
|
1569
|
+
makefile_ext_list = known_file_ext_dict.get('makefile', [])
|
|
1545
1570
|
|
|
1546
1571
|
if forced_extension:
|
|
1547
1572
|
# If forced_extension='systemverilog', then use the first known extension for
|
|
@@ -1581,6 +1606,12 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1581
1606
|
caller_info=caller_info)
|
|
1582
1607
|
dp.apply_args(args_list=[f'-f={file_abspath}'])
|
|
1583
1608
|
del dp
|
|
1609
|
+
elif file_ext in py_file_ext_list:
|
|
1610
|
+
self.files_py.append(file_abspath)
|
|
1611
|
+
util.debug(f"Added Python file {filename} as {file_abspath}")
|
|
1612
|
+
elif file_ext in makefile_ext_list or os.path.split(filename)[1] == 'Makefile':
|
|
1613
|
+
self.files_makefile.append(file_abspath)
|
|
1614
|
+
util.debug(f"Added Makefile {filename} as {file_abspath}")
|
|
1584
1615
|
else:
|
|
1585
1616
|
# unknown file extension. In these cases we link the file to the working directory
|
|
1586
1617
|
# so it is available (for example, a .mem file that is expected to exist with relative
|
|
@@ -2113,21 +2144,22 @@ class CommandParallel(Command):
|
|
|
2113
2144
|
work_queue.put((jobs_launched, command_list, job['name'], cwd))
|
|
2114
2145
|
suffix = "<START>"
|
|
2115
2146
|
if fancy_mode:
|
|
2116
|
-
util.fancy_print(job_text+suffix, worker)
|
|
2147
|
+
util.fancy_print(job_text + suffix, worker)
|
|
2117
2148
|
elif failed_jobs:
|
|
2118
2149
|
# if we aren't in fancy mode, we will print a START line, periodic RUNNING
|
|
2119
2150
|
# lines, and PASS/FAIL line per-job
|
|
2120
|
-
util.
|
|
2151
|
+
util.print_yellow(job_text, end='')
|
|
2152
|
+
util.print_foreground_color(suffix)
|
|
2121
2153
|
else:
|
|
2122
|
-
util.
|
|
2154
|
+
util.print_foreground_color(job_text + suffix)
|
|
2123
2155
|
else:
|
|
2124
2156
|
# single-threaded job launch, we are going to print out job info as we start
|
|
2125
2157
|
# each job... no newline. since non-verbose silences the job and prints only
|
|
2126
2158
|
# <PASS>/<FAIL> after the trailing "..." we leave here
|
|
2127
2159
|
if failed_jobs:
|
|
2128
|
-
util.print_orange(job_text, end="")
|
|
2129
|
-
else:
|
|
2130
2160
|
util.print_yellow(job_text, end="")
|
|
2161
|
+
else:
|
|
2162
|
+
util.print_foreground_color(job_text, end="")
|
|
2131
2163
|
job_done_number = jobs_launched
|
|
2132
2164
|
job_done_name = job['name']
|
|
2133
2165
|
job_start_time = time.time()
|
|
@@ -2181,9 +2213,10 @@ class CommandParallel(Command):
|
|
|
2181
2213
|
if fancy_mode:
|
|
2182
2214
|
util.fancy_print(f"{job_text}{suffix}", t['worker'])
|
|
2183
2215
|
elif failed_jobs:
|
|
2184
|
-
util.
|
|
2216
|
+
util.print_yellow(job_text, end='')
|
|
2217
|
+
util.print_foreground_color(suffix)
|
|
2185
2218
|
else:
|
|
2186
|
-
util.
|
|
2219
|
+
util.print_foreground_color(job_text + suffix)
|
|
2187
2220
|
|
|
2188
2221
|
# shared job completion code
|
|
2189
2222
|
# single or multi-threaded, we can arrive here to harvest <= 1 jobs, and need
|
|
@@ -2191,36 +2224,52 @@ class CommandParallel(Command):
|
|
|
2191
2224
|
# printed, ready for pass/fail
|
|
2192
2225
|
if job_done:
|
|
2193
2226
|
jobs_complete += 1
|
|
2227
|
+
this_job_failed = False
|
|
2194
2228
|
if job_done_return_code is None or job_done_return_code:
|
|
2195
|
-
# embed the color code, to change color of pass/fail during the
|
|
2196
|
-
# util.print_orange/yellow below
|
|
2197
2229
|
if job_done_return_code == 124:
|
|
2198
2230
|
# bash uses 124 for bash timeout errors, if that was preprended to the
|
|
2199
2231
|
# command list.
|
|
2200
|
-
suffix =
|
|
2232
|
+
suffix = (
|
|
2233
|
+
f"<TOUT{safe_emoji(' ❌')}:"
|
|
2234
|
+
f" {sprint_time(job_done_run_time)}>"
|
|
2235
|
+
)
|
|
2236
|
+
this_job_failed = True
|
|
2201
2237
|
else:
|
|
2202
|
-
suffix =
|
|
2238
|
+
suffix = (
|
|
2239
|
+
f"<FAIL{safe_emoji(' ❌')}:"
|
|
2240
|
+
f" {sprint_time(job_done_run_time)}>"
|
|
2241
|
+
)
|
|
2242
|
+
this_job_failed = True
|
|
2203
2243
|
failed_jobs.append(job_done_name)
|
|
2204
2244
|
else:
|
|
2205
|
-
suffix =
|
|
2245
|
+
suffix = (
|
|
2246
|
+
f"<PASS{safe_emoji(' ✅')}:"
|
|
2247
|
+
f" {sprint_time(job_done_run_time)}>"
|
|
2248
|
+
)
|
|
2206
2249
|
passed_jobs.append(job_done_name)
|
|
2207
2250
|
# we want to print in one shot, because in fancy modes that's all that we're allowed
|
|
2208
2251
|
job_done_text = "" if job_done_quiet else sprint_job_line(job_done_number,
|
|
2209
2252
|
job_done_name)
|
|
2210
|
-
if
|
|
2211
|
-
util.
|
|
2212
|
-
|
|
2253
|
+
if this_job_failed:
|
|
2254
|
+
util.print_red(f"{job_done_text}{suffix}")
|
|
2255
|
+
elif failed_jobs:
|
|
2213
2256
|
util.print_yellow(f"{job_done_text}{suffix}")
|
|
2257
|
+
else:
|
|
2258
|
+
util.print_green(f"{job_done_text}{suffix}")
|
|
2214
2259
|
self.jobs_status[job_done_number-1] = job_done_return_code
|
|
2215
2260
|
|
|
2216
2261
|
if not anything_done:
|
|
2217
2262
|
time.sleep(0.25) # if nothing happens for an iteration, chill out a bit
|
|
2218
2263
|
|
|
2219
2264
|
if total_jobs:
|
|
2220
|
-
|
|
2221
|
-
|
|
2265
|
+
if len(passed_jobs) == total_jobs:
|
|
2266
|
+
emojitxt = safe_emoji('😀', ':)')
|
|
2267
|
+
else:
|
|
2268
|
+
emojitxt = safe_emoji('😦', ':(')
|
|
2269
|
+
util.info(sprint_job_line(final=True, job_name="jobs passed") + f"< {emojitxt} >",
|
|
2270
|
+
start="")
|
|
2222
2271
|
else:
|
|
2223
|
-
util.info("Parallel: <No jobs found>")
|
|
2272
|
+
util.info(f"Parallel: <{safe_emoji('❓ ')}No jobs found>")
|
|
2224
2273
|
# Make sure all jobs have a set status:
|
|
2225
2274
|
for i, rc in enumerate(self.jobs_status):
|
|
2226
2275
|
if rc is None or not isinstance(rc, int):
|