opencos-eda 0.2.52__py3-none-any.whl → 0.2.53__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 +3 -0
- 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 +32 -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 +159 -20
- 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 +13 -2
- opencos/tests/test_tools.py +159 -132
- opencos/tools/cocotb.py +9 -0
- opencos/tools/modelsim_ase.py +67 -51
- opencos/tools/quartus.py +638 -0
- opencos/tools/questa.py +167 -88
- opencos/tools/questa_fse.py +10 -0
- opencos/tools/riviera.py +1 -0
- opencos/tools/vivado.py +3 -3
- opencos/util.py +20 -3
- opencos/utils/str_helpers.py +85 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/METADATA +1 -1
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/RECORD +41 -39
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/WHEEL +0 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/entry_points.txt +0 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.2.52.dist-info → opencos_eda-0.2.53.dist-info}/top_level.txt +0 -0
opencos/deps/deps_processor.py
CHANGED
|
@@ -103,6 +103,37 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
103
103
|
self.command_design_ref.incdirs.append(abspath)
|
|
104
104
|
debug(f'Added include dir {abspath} from {self.caller_info}')
|
|
105
105
|
|
|
106
|
+
|
|
107
|
+
def _apply_args_check_tools(self, tokens: list) -> list:
|
|
108
|
+
'''Helper for apply_args(list), returns list strips --tool args under certain conditions'''
|
|
109
|
+
|
|
110
|
+
_orig_tool = self.args.get('tool', '')
|
|
111
|
+
if not self.command_design_ref.auto_tool_applied and \
|
|
112
|
+
any(x.startswith('--tool') for x in tokens) and _orig_tool:
|
|
113
|
+
warn_tool = ''
|
|
114
|
+
for i, item in enumerate(list(tokens)):
|
|
115
|
+
if item == '--tool':
|
|
116
|
+
if tokens[i + 1] != _orig_tool:
|
|
117
|
+
warn_tool = tokens[i + 1]
|
|
118
|
+
tokens[i : i+2] = ['', ''] # remove this and next arg
|
|
119
|
+
elif item.startswith('--tool='):
|
|
120
|
+
if item[7:] != _orig_tool:
|
|
121
|
+
warn_tool = item
|
|
122
|
+
tokens[i] = '' # remove just this arg.
|
|
123
|
+
|
|
124
|
+
if warn_tool:
|
|
125
|
+
warning(
|
|
126
|
+
f'Attempting to set --tool {warn_tool} from DEPS',
|
|
127
|
+
f'(file={self.deps_file}:{self.target_node})',
|
|
128
|
+
f'however the tool was already chosen as: {_orig_tool}. The --tool arg will',
|
|
129
|
+
f'not be applied from: {tokens}'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
tokens = [item for item in tokens if item != ''] # remove blanks
|
|
133
|
+
|
|
134
|
+
return tokens
|
|
135
|
+
|
|
136
|
+
|
|
106
137
|
def apply_args(self, args_list:list) -> list:
|
|
107
138
|
'''Given args_list, applies them to our self.command_design_ref obj
|
|
108
139
|
|
|
@@ -114,9 +145,20 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
114
145
|
self.error(f"{args_list=} is not type str/list, can't apply args",
|
|
115
146
|
f"in {self.caller_info}")
|
|
116
147
|
tokens = dep_str2list(args_list)
|
|
148
|
+
|
|
117
149
|
# We're going to run an ArgumentParser here, which is not the most efficient
|
|
118
150
|
# thing to do b/c it runs on all of self.command_design_ref.args (dict) even
|
|
119
151
|
# if we're applying a single token.
|
|
152
|
+
|
|
153
|
+
# We have to special-case anything with --tool[=value] in tokens, otherwise
|
|
154
|
+
# the user may think they were allowed to set --tool, but in our flow the Command handler
|
|
155
|
+
# (self.command_design_ref) has already been chosen, so setting the tool can have
|
|
156
|
+
# strange side-effects.
|
|
157
|
+
_orig_tool = self.args.get('tool', '')
|
|
158
|
+
tokens = self._apply_args_check_tools(tokens=tokens)
|
|
159
|
+
if not tokens:
|
|
160
|
+
return []
|
|
161
|
+
|
|
120
162
|
debug(f'deps_processor - custom apply_args with {tokens=}',
|
|
121
163
|
f'from {self.caller_info}')
|
|
122
164
|
_, unparsed = self.command_design_ref.run_argparser_on_list(
|
|
@@ -135,6 +177,16 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
135
177
|
# flist from that target.
|
|
136
178
|
warning(f'For {self.command_design_ref.command_name}:' \
|
|
137
179
|
+ f' in {self.caller_info} has unknown args {unparsed=}')
|
|
180
|
+
|
|
181
|
+
if self.command_design_ref.auto_tool_applied and _orig_tool != self.args.get('tool', ''):
|
|
182
|
+
debug(f'deps_processor.apply_args: tool changed, {self.args["tool"]=}, will attempt',
|
|
183
|
+
f'to respawn the job using original args: {self.config["eda_original_args"]}')
|
|
184
|
+
self.command_design_ref.tool_changed_respawn = {
|
|
185
|
+
'tool': self.args['tool'],
|
|
186
|
+
'orig_tool': _orig_tool,
|
|
187
|
+
'from': self.caller_info,
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
return unparsed
|
|
139
191
|
|
|
140
192
|
def apply_reqs(self, reqs_list:list) -> None:
|
|
@@ -213,6 +265,10 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
213
265
|
elif key == 'deps':
|
|
214
266
|
remaining_deps_list += self.process_deps_return_discovered_deps()
|
|
215
267
|
|
|
268
|
+
if self.command_design_ref.tool_changed_respawn:
|
|
269
|
+
# Stop now, and have eda.py respawn the command.
|
|
270
|
+
return []
|
|
271
|
+
|
|
216
272
|
# We return the list of deps that still need to be resolved (['full_path/some_target', ...])
|
|
217
273
|
return remaining_deps_list
|
|
218
274
|
|
|
@@ -260,6 +316,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
260
316
|
|
|
261
317
|
|
|
262
318
|
apply_tag_items_tools = False
|
|
319
|
+
apply_tag_items_commands = False
|
|
263
320
|
apply_tag_items_with_args = False
|
|
264
321
|
|
|
265
322
|
tool = self.args.get('tool', None)
|
|
@@ -276,6 +333,12 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
276
333
|
warning(f'{tagname=} in {self.caller_info}:',
|
|
277
334
|
' skipped due to with-tools disabled.')
|
|
278
335
|
|
|
336
|
+
with_commands = dep_str2list(value.get('with-commands', []))
|
|
337
|
+
if with_commands and not deps_tags_enables.get('with-commands', None):
|
|
338
|
+
with_commands = []
|
|
339
|
+
warning(f'{tagname=} in {self.caller_info}:',
|
|
340
|
+
' skipped due to with-commands disabled.')
|
|
341
|
+
|
|
279
342
|
with_args = value.get('with-args', {})
|
|
280
343
|
if not isinstance(with_args, dict):
|
|
281
344
|
error(f'{tagname=} in {self.caller_info}:',
|
|
@@ -299,6 +362,12 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
299
362
|
if tool_full_version and tool_full_version in with_tools:
|
|
300
363
|
apply_tag_items_tools = True
|
|
301
364
|
|
|
365
|
+
# check with-commands?
|
|
366
|
+
if not with_commands:
|
|
367
|
+
apply_tag_items_commands = True # no with-commands present
|
|
368
|
+
elif getattr(self.command_design_ref, 'command_name', '') in with_commands:
|
|
369
|
+
apply_tag_items_commands = True # with-commands present and we matched.
|
|
370
|
+
|
|
302
371
|
# check with-args?
|
|
303
372
|
with_args_matched_list = []
|
|
304
373
|
for k,v in with_args.items():
|
|
@@ -336,7 +405,8 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
336
405
|
elif enable_tags_matched:
|
|
337
406
|
apply_tag_items = True
|
|
338
407
|
else:
|
|
339
|
-
apply_tag_items = apply_tag_items_tools
|
|
408
|
+
apply_tag_items = all([apply_tag_items_tools, apply_tag_items_with_args,
|
|
409
|
+
apply_tag_items_commands])
|
|
340
410
|
|
|
341
411
|
if not apply_tag_items:
|
|
342
412
|
debug(f'process_tags(): {tagname=} in {self.caller_info}',
|
|
@@ -370,7 +440,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
|
|
|
370
440
|
if args_list:
|
|
371
441
|
# This will apply knowns args to the target dep:
|
|
372
442
|
info(f'{tagname=} in {self.caller_info=}:',
|
|
373
|
-
f'applying args
|
|
443
|
+
f'applying args for {args_list=}')
|
|
374
444
|
self.apply_args(args_list)
|
|
375
445
|
|
|
376
446
|
elif key == 'reqs':
|
opencos/deps_schema.py
CHANGED
|
@@ -111,6 +111,8 @@ my_target_name:
|
|
|
111
111
|
<str>: <---- string user name for this tag
|
|
112
112
|
with-tools: <---- optional array (or string) of tools this tag requires
|
|
113
113
|
for the tag to be applied.
|
|
114
|
+
with-commands: <---- optional array (or string) of command this tag requires
|
|
115
|
+
for the tag to be applied.
|
|
114
116
|
with-args: <---- optional table of args values that must match for
|
|
115
117
|
this tag to be applied.
|
|
116
118
|
args: <---- optional array of args to be applied to this target
|
|
@@ -217,6 +219,7 @@ TARGET_EDA_COMMAND = {
|
|
|
217
219
|
TARGET_TAGS_TABLE = {
|
|
218
220
|
Optional(str): {
|
|
219
221
|
Optional('with-tools'): ARRAY_OR_SPACE_SEPARATED_STRING,
|
|
222
|
+
Optional('with-commands'): ARRAY_OR_SPACE_SEPARATED_STRING,
|
|
220
223
|
Optional('with-args'): dict,
|
|
221
224
|
Optional('args'): ARRAY_OR_SPACE_SEPARATED_STRING,
|
|
222
225
|
Optional('deps'): ARRAY_OR_SPACE_SEPARATED_STRING,
|
opencos/eda.py
CHANGED
|
@@ -20,13 +20,19 @@ from pathlib import Path
|
|
|
20
20
|
|
|
21
21
|
import opencos
|
|
22
22
|
from opencos import util, eda_config, eda_base
|
|
23
|
-
from opencos.eda_base import Tool, which_tool
|
|
23
|
+
from opencos.eda_base import Tool, which_tool, get_eda_exec
|
|
24
24
|
from opencos.utils import vsim_helper, vscode_helper
|
|
25
|
+
from opencos.utils.subprocess_helpers import subprocess_run_background
|
|
26
|
+
from opencos.utils import status_constants
|
|
25
27
|
|
|
26
28
|
# Configure util:
|
|
27
29
|
util.progname = "EDA"
|
|
28
30
|
util.global_log.default_log_enabled = True
|
|
29
31
|
util.global_log.default_log_filepath = os.path.join('eda.work', 'eda.log')
|
|
32
|
+
util.global_log.default_log_disable_with_args.extend([
|
|
33
|
+
# avoid default log on certain eda commands
|
|
34
|
+
'help', 'waves', 'deps-help', 'targets'
|
|
35
|
+
])
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
# ******************************************************************************
|
|
@@ -44,12 +50,12 @@ def init_config(
|
|
|
44
50
|
# For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
|
|
45
51
|
# the actual class using importlib (via opencos.util)
|
|
46
52
|
config['command_handler'] = {}
|
|
47
|
-
for
|
|
53
|
+
for _cmd, str_class in config['DEFAULT_HANDLERS'].items():
|
|
48
54
|
cls = util.import_class_from_string(str_class)
|
|
49
55
|
if not cls:
|
|
50
|
-
util.error(f"config DEFAULT_HANDLERS
|
|
56
|
+
util.error(f"config DEFAULT_HANDLERS command={_cmd} {str_class=} could not import")
|
|
51
57
|
else:
|
|
52
|
-
config['command_handler'][
|
|
58
|
+
config['command_handler'][_cmd] = cls
|
|
53
59
|
|
|
54
60
|
config['auto_tools_found'] = {}
|
|
55
61
|
config['tools_loaded'] = set()
|
|
@@ -58,6 +64,18 @@ def init_config(
|
|
|
58
64
|
return config
|
|
59
65
|
|
|
60
66
|
|
|
67
|
+
def get_all_commands_help_str(config: dict) -> str:
|
|
68
|
+
'''Returns a str of help based on what commands eda supports, from config'''
|
|
69
|
+
all_commands_help = []
|
|
70
|
+
max_command_str_len = max(len(s) for s in config.get('DEFAULT_HANDLERS_HELP', {}).keys())
|
|
71
|
+
for key, value in config.get('DEFAULT_HANDLERS_HELP', {}).items():
|
|
72
|
+
all_commands_help.append(f' {key:<{max_command_str_len}} - {value.strip()}')
|
|
73
|
+
if all_commands_help:
|
|
74
|
+
all_commands_help = [
|
|
75
|
+
'Where <command> is one of:',
|
|
76
|
+
'',
|
|
77
|
+
] + all_commands_help
|
|
78
|
+
return '\n'.join(all_commands_help)
|
|
61
79
|
|
|
62
80
|
|
|
63
81
|
def usage(tokens: list, config: dict, command="") -> int:
|
|
@@ -77,26 +95,11 @@ def usage(tokens: list, config: dict, command="") -> int:
|
|
|
77
95
|
"""
|
|
78
96
|
Usage:
|
|
79
97
|
eda [<options>] <command> [options] <files|targets, ...>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
synth - Synthesizes a DEPS target
|
|
86
|
-
flist - Create dependency from a DEPS target
|
|
87
|
-
proj - Create a project from a DEPS target for GUI sim/waves/debug
|
|
88
|
-
multi - Run multiple DEPS targets, serially or in parallel
|
|
89
|
-
tools-multi - Same as 'multi' but run on all available tools, or specfied using --tools
|
|
90
|
-
sweep - Sweep one or more arguments across a range, serially or in parallel
|
|
91
|
-
build - Build for a board, creating a project and running build flow
|
|
92
|
-
waves - Opens waveform from prior simulation
|
|
93
|
-
upload - Uploads a finished design into hardware
|
|
94
|
-
open - Opens a project
|
|
95
|
-
export - Export files related to a target, tool independent
|
|
96
|
-
shell - Runs only commands for DEPS target (like sim or elab, but stops prior to tool)
|
|
97
|
-
targets - list all possible targets given glob path.
|
|
98
|
-
help - This help (without args), or i.e. "eda help sim" for specific help
|
|
99
|
-
|
|
98
|
+
"""
|
|
99
|
+
)
|
|
100
|
+
print(get_all_commands_help_str(config=config))
|
|
101
|
+
print(
|
|
102
|
+
"""
|
|
100
103
|
And <files|targets, ...> is one or more source file or DEPS markup file target,
|
|
101
104
|
such as .v, .sv, .vhd[l], .cpp files, or a target key in a DEPS.[yml|yaml|toml|json].
|
|
102
105
|
Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` to
|
|
@@ -249,7 +252,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
|
|
|
249
252
|
p = shutil.which(exe)
|
|
250
253
|
config['auto_tools_found'][name] = exe # populate key-value pairs w/ first exe in list
|
|
251
254
|
if not quiet:
|
|
252
|
-
util.info(f"Detected {name} ({p})
|
|
255
|
+
util.info(f"Detected {name} ({p})")
|
|
253
256
|
tool_setup(tool=name, quiet=True, auto_setup=True, warnings=warnings, config=config)
|
|
254
257
|
|
|
255
258
|
return config
|
|
@@ -317,7 +320,7 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
|
|
|
317
320
|
config['tools_loaded'].add(tool)
|
|
318
321
|
|
|
319
322
|
|
|
320
|
-
def process_tokens( # pylint: disable=too-many-branches,too-many-statements
|
|
323
|
+
def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-many-locals
|
|
321
324
|
tokens: list, original_args: list, config: dict, is_interactive=False
|
|
322
325
|
) -> int:
|
|
323
326
|
'''Returns bash/sh style return code int (0 pass, non-zero fail).
|
|
@@ -335,6 +338,7 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements
|
|
|
335
338
|
deferred_tokens = []
|
|
336
339
|
command = ""
|
|
337
340
|
run_auto_tool_setup = True
|
|
341
|
+
process_tokens_cwd = os.getcwd()
|
|
338
342
|
|
|
339
343
|
parser = eda_base.get_argparser()
|
|
340
344
|
try:
|
|
@@ -409,6 +413,7 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements
|
|
|
409
413
|
command not in config.get('command_tool_is_optional', []):
|
|
410
414
|
use_tool = which_tool(command, config)
|
|
411
415
|
util.info(f"--tool not specified, using default for {command=}: {use_tool}")
|
|
416
|
+
setattr(sco, 'auto_tool_applied', True)
|
|
412
417
|
|
|
413
418
|
rc = check_command_handler_cls(command_obj=sco, command=command, parsed_args=parsed)
|
|
414
419
|
if rc > 0:
|
|
@@ -426,6 +431,25 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements
|
|
|
426
431
|
# rc=1 because that's the python exception rc)
|
|
427
432
|
rc = getattr(sco, 'status', 2)
|
|
428
433
|
util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
|
|
434
|
+
|
|
435
|
+
if rc == 0 and not parsed.tool and getattr(sco, 'tool_changed_respawn', False):
|
|
436
|
+
use_tool = sco.args.get('tool', '')
|
|
437
|
+
if not use_tool:
|
|
438
|
+
util.error(f'Unable to change tool from {parsed.tool}, internal eda problem.')
|
|
439
|
+
return status_constants.EDA_DEFAULT_ERROR
|
|
440
|
+
|
|
441
|
+
# close the util.log:
|
|
442
|
+
util.stop_log()
|
|
443
|
+
# respawn the original job, but with --tool=<use_tool> applied:
|
|
444
|
+
_command_list = [get_eda_exec(command), f"--tool={use_tool}"] + original_args
|
|
445
|
+
util.info(f'eda: respawn for tool change: {" ".join(_command_list)};',
|
|
446
|
+
f' (running from: {process_tokens_cwd})')
|
|
447
|
+
subprocess_run_background(
|
|
448
|
+
work_dir=process_tokens_cwd,
|
|
449
|
+
command_list=_command_list,
|
|
450
|
+
background=util.args.get('quiet', False)
|
|
451
|
+
)
|
|
452
|
+
|
|
429
453
|
return rc
|
|
430
454
|
|
|
431
455
|
|
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
|
|
@@ -245,6 +256,8 @@ class Command:
|
|
|
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.
|
|
@@ -1345,10 +1450,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1345
1450
|
plusarg = strip_outer_quotes(token)
|
|
1346
1451
|
self.process_plusarg(plusarg, pwd=pwd)
|
|
1347
1452
|
remove_list.append(token)
|
|
1453
|
+
|
|
1348
1454
|
for x in remove_list:
|
|
1349
1455
|
unparsed.remove(x)
|
|
1350
1456
|
|
|
1351
|
-
if not unparsed
|
|
1457
|
+
if self.error_on_no_files_or_targets and not unparsed:
|
|
1352
1458
|
# derived classes can set error_on_no_files_or_targets=True
|
|
1353
1459
|
# For example: CommandSim will error (requires files/targets),
|
|
1354
1460
|
# CommandWaves does not (files/targets not required)
|
|
@@ -1367,13 +1473,27 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1367
1473
|
util.warning(f"For command '{self.command_name}' no files or targets were",
|
|
1368
1474
|
f"presented at the command line, so using '{target}' from",
|
|
1369
1475
|
f"{deps.deps_file}")
|
|
1370
|
-
if
|
|
1476
|
+
if not unparsed:
|
|
1371
1477
|
# If unparsed is still empty, then error.
|
|
1372
1478
|
self.error(f"For command '{self.command_name}' no files or targets were",
|
|
1373
1479
|
f"presented at the command line: {orig_tokens}",
|
|
1374
1480
|
error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
|
|
1375
1481
|
|
|
1376
1482
|
# by this point hopefully this is a target ... is it a simple filename?
|
|
1483
|
+
|
|
1484
|
+
# Before we look for files, check for stray --some-arg in unparsed, we don't want to treat
|
|
1485
|
+
# these as potential targets if process_all=True, but someone might have a file named
|
|
1486
|
+
# --my_file.sv, so those are technically allowed until the tool would fail on them.
|
|
1487
|
+
possible_unparsed_args = [
|
|
1488
|
+
x for x in unparsed if x.startswith('--') and not os.path.isfile(x)
|
|
1489
|
+
]
|
|
1490
|
+
if process_all and possible_unparsed_args:
|
|
1491
|
+
_tool = self.safe_which_tool()
|
|
1492
|
+
self.warning_show_known_args()
|
|
1493
|
+
self.error(f"Didn't understand unparsed args: {possible_unparsed_args}, for command",
|
|
1494
|
+
f"'{self.command_name}', tool '{_tool}'",
|
|
1495
|
+
error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
|
|
1496
|
+
|
|
1377
1497
|
remove_list = []
|
|
1378
1498
|
last_potential_top_file = ('', '') # (top, fpath)
|
|
1379
1499
|
last_potential_top_target = ('', '') # (top, path/to/full-target-name)
|
|
@@ -1405,6 +1525,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1405
1525
|
|
|
1406
1526
|
# we appear to be dealing with a target name which needs to be resolved (usually
|
|
1407
1527
|
# recursively)
|
|
1528
|
+
if token.startswith('-'):
|
|
1529
|
+
# We are not going to handle targets that start with a -, it's likely
|
|
1530
|
+
# an unparsed arg.
|
|
1531
|
+
continue
|
|
1408
1532
|
if token.startswith(os.sep):
|
|
1409
1533
|
target_name = token # if it's absolute path, don't prepend anything
|
|
1410
1534
|
else:
|
|
@@ -1424,8 +1548,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1424
1548
|
unparsed.remove(x)
|
|
1425
1549
|
|
|
1426
1550
|
# we were unable to figure out what this command line token is for...
|
|
1427
|
-
if process_all and
|
|
1428
|
-
self.
|
|
1551
|
+
if process_all and unparsed:
|
|
1552
|
+
self.warning_show_known_args()
|
|
1553
|
+
self.error(f"Didn't understand remaining args or targets {unparsed=} for command",
|
|
1554
|
+
f"'{self.command_name}'",
|
|
1429
1555
|
error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
|
|
1430
1556
|
|
|
1431
1557
|
# handle a missing self.args['top'] with last filepath or last target:
|
|
@@ -1462,6 +1588,14 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
|
|
|
1462
1588
|
f"'{self.command_name}' for tool={self.args.get('tool', None)}",
|
|
1463
1589
|
error_code=status_constants.EDA_COMMAND_MISSING_TOP)
|
|
1464
1590
|
|
|
1591
|
+
if self.tool_changed_respawn:
|
|
1592
|
+
util.warning(
|
|
1593
|
+
'CommandDesign: need to respawn due to tool change to',
|
|
1594
|
+
f'\'{self.tool_changed_respawn["tool"]}\' from',
|
|
1595
|
+
f'\'{self.tool_changed_respawn["orig_tool"]}\'',
|
|
1596
|
+
f'(from DEPS, {self.tool_changed_respawn["from"]})'
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1465
1599
|
return unparsed
|
|
1466
1600
|
|
|
1467
1601
|
|
|
@@ -1961,6 +2095,7 @@ class CommandParallel(Command):
|
|
|
1961
2095
|
# There should not be any single_cmd_unparsed args starting with '-'
|
|
1962
2096
|
bad_remaining_args = [x for x in single_cmd_unparsed if x.startswith('-')]
|
|
1963
2097
|
if bad_remaining_args:
|
|
2098
|
+
self.warning_show_known_args(command=f'{self.command_name} {command}')
|
|
1964
2099
|
self.error(f'for {self.command_name} {command=} the following args are unknown',
|
|
1965
2100
|
f'{bad_remaining_args}',
|
|
1966
2101
|
error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
|
|
@@ -2020,8 +2155,12 @@ class CommandParallel(Command):
|
|
|
2020
2155
|
tpath, _ = os.path.split(job_dict['target'])
|
|
2021
2156
|
|
|
2022
2157
|
# prepend path information to job-name:
|
|
2023
|
-
patched_target_path = os.path.relpath(tpath).replace(os.sep, '_')
|
|
2024
|
-
|
|
2158
|
+
patched_target_path = os.path.relpath(tpath).replace(os.sep, '_').lstrip('.')
|
|
2159
|
+
if patched_target_path:
|
|
2160
|
+
new_job_name = f'{patched_target_path}.{key}'
|
|
2161
|
+
else:
|
|
2162
|
+
continue # there's nothing to "patch", our job-name will be unchanged.
|
|
2163
|
+
|
|
2025
2164
|
replace_job_arg(job_dict, arg_name='job-name', new_value=new_job_name)
|
|
2026
2165
|
|
|
2027
2166
|
# prepend path information to force-logfile (if present):
|