opencos-eda 0.2.36__py3-none-any.whl → 0.2.39__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/multi.py +7 -3
- opencos/commands/sweep.py +12 -4
- opencos/commands/targets.py +49 -0
- opencos/eda.py +39 -99
- opencos/eda_base.py +111 -19
- opencos/eda_config.py +20 -11
- opencos/eda_config_defaults.yml +28 -1
- opencos/eda_config_reduced.yml +0 -1
- opencos/eda_deps_bash_completion.bash +1 -1
- opencos/eda_extract_targets.py +37 -19
- opencos/files.py +2 -2
- opencos/tests/helpers.py +7 -6
- opencos/tests/test_eda.py +22 -0
- opencos/tools/invio.py +4 -5
- opencos/tools/invio_yosys.py +28 -20
- opencos/tools/iverilog.py +4 -2
- opencos/tools/modelsim_ase.py +4 -3
- opencos/tools/slang_yosys.py +10 -135
- opencos/tools/verilator.py +10 -8
- opencos/tools/vivado.py +0 -1
- opencos/tools/yosys.py +135 -0
- {opencos_eda-0.2.36.dist-info → opencos_eda-0.2.39.dist-info}/METADATA +1 -1
- {opencos_eda-0.2.36.dist-info → opencos_eda-0.2.39.dist-info}/RECORD +29 -28
- {opencos_eda-0.2.36.dist-info → opencos_eda-0.2.39.dist-info}/WHEEL +0 -0
- {opencos_eda-0.2.36.dist-info → opencos_eda-0.2.39.dist-info}/entry_points.txt +0 -0
- {opencos_eda-0.2.36.dist-info → opencos_eda-0.2.39.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.2.36.dist-info → opencos_eda-0.2.39.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.2.36.dist-info → opencos_eda-0.2.39.dist-info}/top_level.txt +0 -0
opencos/commands/__init__.py
CHANGED
|
@@ -17,6 +17,7 @@ from .sweep import CommandSweep
|
|
|
17
17
|
from .synth import CommandSynth
|
|
18
18
|
from .upload import CommandUpload
|
|
19
19
|
from .waves import CommandWaves
|
|
20
|
+
from .targets import CommandTargets
|
|
20
21
|
|
|
21
22
|
__all__ = [
|
|
22
23
|
'CommandBuild',
|
|
@@ -32,4 +33,5 @@ __all__ = [
|
|
|
32
33
|
'CommandToolsMulti',
|
|
33
34
|
'CommandUpload',
|
|
34
35
|
'CommandWaves',
|
|
36
|
+
'CommandTargets',
|
|
35
37
|
]
|
opencos/commands/multi.py
CHANGED
|
@@ -392,7 +392,9 @@ class CommandMulti(CommandParallel):
|
|
|
392
392
|
# Special case for 'multi' --export-jsonl, run reach child with --export-json
|
|
393
393
|
command_list.append('--export-json')
|
|
394
394
|
if tool and len(all_multi_tools) > 1:
|
|
395
|
-
command_list.append(f'--
|
|
395
|
+
command_list.append(f'--job-name={short_target}.{command}.{tool}')
|
|
396
|
+
else:
|
|
397
|
+
command_list.append(f'--job-name={short_target}.{command}')
|
|
396
398
|
|
|
397
399
|
def append_jobs_from_targets(self, args:list):
|
|
398
400
|
'''Helper method in CommandMulti to apply 'args' (list) to all self.targets,
|
|
@@ -430,13 +432,15 @@ class CommandMulti(CommandParallel):
|
|
|
430
432
|
type(self.args['single-timeout']) in [int, str]:
|
|
431
433
|
command_list = ['timeout', str(self.args['single-timeout'])] + command_list
|
|
432
434
|
|
|
433
|
-
name = target
|
|
435
|
+
name = self.get_name_from_target(target)
|
|
434
436
|
if tool and (len(all_multi_tools) > 1 or self.command_name == 'tools-multi'):
|
|
435
|
-
name
|
|
437
|
+
name += f' ({tool})'
|
|
436
438
|
|
|
437
439
|
this_job_dict = {
|
|
438
440
|
'name' : name,
|
|
439
441
|
'index' : len(self.jobs),
|
|
442
|
+
'command': command,
|
|
443
|
+
'target': target,
|
|
440
444
|
'command_list' : command_list
|
|
441
445
|
}
|
|
442
446
|
if tool:
|
opencos/commands/sweep.py
CHANGED
|
@@ -113,6 +113,9 @@ class CommandSweep(CommandDesign, CommandParallel):
|
|
|
113
113
|
util.debug(f"Sweep: arg_tokens: '{arg_tokens}'")
|
|
114
114
|
util.debug(f"Sweep: target: '{self.sweep_target}'")
|
|
115
115
|
|
|
116
|
+
if not self.sweep_target:
|
|
117
|
+
self.error(f"Sweep can only take one target, found none, args: {unparsed}")
|
|
118
|
+
|
|
116
119
|
# now create the list of jobs, support one axis
|
|
117
120
|
self.jobs = []
|
|
118
121
|
|
|
@@ -197,15 +200,20 @@ class CommandSweep(CommandDesign, CommandParallel):
|
|
|
197
200
|
f"target={self.sweep_target}, arg_tokens={arg_tokens},",
|
|
198
201
|
f"sweep_axis_list={sweep_axis_list}")
|
|
199
202
|
if not sweep_axis_list:
|
|
200
|
-
# we aren't sweeping anything, create one job
|
|
201
|
-
|
|
203
|
+
# we aren't sweeping anything, create one job:
|
|
204
|
+
# name {target}.{command}[.sweep_value,.sweep_value,...]
|
|
205
|
+
snapshot_name = self.get_name_from_target(self.sweep_target)
|
|
206
|
+
snapshot_name = snapshot_name.replace(os.sep, '_') \
|
|
207
|
+
+ f'.{self.single_command}{sweep_string}'
|
|
202
208
|
eda_path = get_eda_exec('sweep')
|
|
203
209
|
self.jobs.append({
|
|
204
210
|
'name' : snapshot_name,
|
|
205
211
|
'index' : len(self.jobs),
|
|
212
|
+
'command': self.single_command,
|
|
213
|
+
'target': self.sweep_target,
|
|
206
214
|
'command_list' : (
|
|
207
215
|
[eda_path, self.single_command, self.sweep_target,
|
|
208
|
-
'--
|
|
216
|
+
f'--job-name={snapshot_name}'] + arg_tokens
|
|
209
217
|
)
|
|
210
218
|
})
|
|
211
219
|
return
|
|
@@ -221,7 +229,7 @@ class CommandSweep(CommandDesign, CommandParallel):
|
|
|
221
229
|
this_arg_tokens.append(f'{lhs}{operator}{v}')
|
|
222
230
|
|
|
223
231
|
v_string = f"{v}".replace('.','p')
|
|
224
|
-
this_sweep_string = sweep_string + f"
|
|
232
|
+
this_sweep_string = sweep_string + f".{lhs_trimmed}_{v_string}"
|
|
225
233
|
|
|
226
234
|
self.expand_sweep_axis(
|
|
227
235
|
arg_tokens=this_arg_tokens,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'''opencos.commands.targets - command handler for: eda targets [args]
|
|
2
|
+
|
|
3
|
+
Note this command is handled differently than others (such as CommandSim),
|
|
4
|
+
it is generally run as simply
|
|
5
|
+
|
|
6
|
+
> eda targets
|
|
7
|
+
> eda targets <directory>
|
|
8
|
+
> eda targets [directory/]<pattern> [directory2/]<pattern2> ...
|
|
9
|
+
|
|
10
|
+
uses no tools and will print a pretty list of targets to stdout.
|
|
11
|
+
'''
|
|
12
|
+
|
|
13
|
+
# Note - similar code waiver, tricky to eliminate it with inheritance when
|
|
14
|
+
# calling reusable methods.
|
|
15
|
+
# pylint: disable=R0801
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
from opencos import eda_extract_targets
|
|
20
|
+
from opencos.eda_base import Command
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CommandTargets:
|
|
24
|
+
'''command handler for: eda targets'''
|
|
25
|
+
|
|
26
|
+
command_name = 'targets'
|
|
27
|
+
|
|
28
|
+
def __init__(self, config: dict):
|
|
29
|
+
# We don't inherit opencos.eda_base.Command, so we have to set a few
|
|
30
|
+
# member vars for Command.help to work.
|
|
31
|
+
self.args = {}
|
|
32
|
+
self.args_help = {}
|
|
33
|
+
self.config = config
|
|
34
|
+
self.status = 0
|
|
35
|
+
|
|
36
|
+
def process_tokens( # pylint: disable=unused-argument
|
|
37
|
+
self, tokens: list, process_all: bool = True,
|
|
38
|
+
pwd: str = os.getcwd()
|
|
39
|
+
) -> list:
|
|
40
|
+
'''This is effectively our 'run' method, entrypoint from opencos.eda.main'''
|
|
41
|
+
|
|
42
|
+
eda_extract_targets.run(partial_paths=tokens, base_path=pwd)
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
def help(self, tokens: list) -> None:
|
|
46
|
+
'''Since we don't inherit from opencos.eda_base.Command, need our own help
|
|
47
|
+
method
|
|
48
|
+
'''
|
|
49
|
+
Command.help(self, tokens=tokens)
|
opencos/eda.py
CHANGED
|
@@ -10,6 +10,7 @@ import re
|
|
|
10
10
|
import signal
|
|
11
11
|
import argparse
|
|
12
12
|
import shlex
|
|
13
|
+
import importlib.util
|
|
13
14
|
|
|
14
15
|
import opencos
|
|
15
16
|
from opencos import util, files
|
|
@@ -19,7 +20,6 @@ from opencos.eda_base import Tool, which_tool
|
|
|
19
20
|
|
|
20
21
|
# Globals
|
|
21
22
|
|
|
22
|
-
debug_respawn = False
|
|
23
23
|
util.progname = "EDA"
|
|
24
24
|
|
|
25
25
|
|
|
@@ -30,37 +30,25 @@ util.progname = "EDA"
|
|
|
30
30
|
# eda command (such as, command: eda sim) is handled by which class (such as class: CommandSim)
|
|
31
31
|
# These are also overriden depending on the tool, for example --tool verilator sets
|
|
32
32
|
# "sim": CommandSimVerilator.
|
|
33
|
-
def init_config(
|
|
33
|
+
def init_config(
|
|
34
|
+
config: dict, quiet: bool = False, tool=None, run_auto_tool_setup: bool = True
|
|
35
|
+
) -> dict:
|
|
34
36
|
'''Sets or clears entries in config (dict) so tools can be re-loaded.'''
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
config['command_handler'] = {
|
|
46
|
-
"sim" : CommandSim,
|
|
47
|
-
"elab" : CommandElab,
|
|
48
|
-
"synth" : CommandSynth,
|
|
49
|
-
"flist" : CommandFList,
|
|
50
|
-
"proj" : CommandProj,
|
|
51
|
-
"multi" : CommandMulti,
|
|
52
|
-
"tools-multi" : CommandToolsMulti,
|
|
53
|
-
"sweep" : CommandSweep,
|
|
54
|
-
"build" : CommandBuild,
|
|
55
|
-
"waves" : CommandWaves,
|
|
56
|
-
"upload" : CommandUpload,
|
|
57
|
-
"open" : CommandOpen,
|
|
58
|
-
"export" : CommandExport,
|
|
59
|
-
}
|
|
38
|
+
# For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
|
|
39
|
+
# the actual class using importlib (via opencos.util)
|
|
40
|
+
config['command_handler'] = {}
|
|
41
|
+
for command, str_class in config['DEFAULT_HANDLERS'].items():
|
|
42
|
+
cls = util.import_class_from_string(str_class)
|
|
43
|
+
if not cls:
|
|
44
|
+
util.error(f"config DEFAULT_HANDLERS {command=} {str_class=} could not import")
|
|
45
|
+
else:
|
|
46
|
+
config['command_handler'][command] = cls
|
|
60
47
|
|
|
61
48
|
config['auto_tools_found'] = dict()
|
|
62
49
|
config['tools_loaded'] = set()
|
|
63
|
-
|
|
50
|
+
if run_auto_tool_setup:
|
|
51
|
+
config = auto_tool_setup(config=config, quiet=quiet, tool=tool)
|
|
64
52
|
return config
|
|
65
53
|
|
|
66
54
|
|
|
@@ -154,7 +142,6 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
|
|
|
154
142
|
tool='verlator', tool='verilator=/path/to/verilator.exe'
|
|
155
143
|
If so, updates config['auto_tools_order'][tool]['exe']
|
|
156
144
|
'''
|
|
157
|
-
import importlib.util
|
|
158
145
|
|
|
159
146
|
tool = eda_config.update_config_auto_tool_order_for_tool(
|
|
160
147
|
tool=tool, config=config
|
|
@@ -237,7 +224,6 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
|
|
|
237
224
|
tool='verlator', tool='verilator=/path/to/verilator.exe'
|
|
238
225
|
|
|
239
226
|
'''
|
|
240
|
-
import importlib
|
|
241
227
|
|
|
242
228
|
tool = eda_config.update_config_auto_tool_order_for_tool(
|
|
243
229
|
tool=tool, config=config
|
|
@@ -281,10 +267,7 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
|
|
|
281
267
|
# skip, already has a tool associated with it, and we're in auto_setup=True
|
|
282
268
|
continue
|
|
283
269
|
|
|
284
|
-
|
|
285
|
-
cls = util.import_class_from_string(str_class_name)
|
|
286
|
-
else:
|
|
287
|
-
cls = globals().get(str_class_name, None)
|
|
270
|
+
cls = util.import_class_from_string(str_class_name)
|
|
288
271
|
|
|
289
272
|
assert issubclass(cls, Tool), f'{str_class_name=} is does not have Tool class associated with it'
|
|
290
273
|
util.debug(f'Setting {cls=} for {command=} in config.command_handler')
|
|
@@ -302,6 +285,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
302
285
|
|
|
303
286
|
deferred_tokens = []
|
|
304
287
|
command = ""
|
|
288
|
+
run_auto_tool_setup = True
|
|
305
289
|
|
|
306
290
|
parser = eda_base.get_argparser()
|
|
307
291
|
try:
|
|
@@ -324,22 +308,29 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
324
308
|
if parsed.eda_safe:
|
|
325
309
|
eda_config.update_config_for_eda_safe(config)
|
|
326
310
|
|
|
311
|
+
util.debug(f'eda process_tokens: {parsed=} {unparsed=}')
|
|
312
|
+
|
|
313
|
+
# Attempt to get the 'command' in the unparsed args before we've even
|
|
314
|
+
# set the command handlers (some commands don't use tools).
|
|
315
|
+
for value in unparsed:
|
|
316
|
+
if value in config['DEFAULT_HANDLERS'].keys():
|
|
317
|
+
command = value
|
|
318
|
+
if value in config['command_uses_no_tools']:
|
|
319
|
+
run_auto_tool_setup = False
|
|
320
|
+
unparsed.remove(value)
|
|
321
|
+
break
|
|
322
|
+
|
|
327
323
|
if not interactive:
|
|
328
324
|
# Run init_config() now, we deferred it in main(), but only run it
|
|
329
325
|
# for this tool (or tool=None to figure it out)
|
|
330
|
-
config = init_config(
|
|
326
|
+
config = init_config(
|
|
327
|
+
config, tool=parsed.tool,
|
|
328
|
+
run_auto_tool_setup=run_auto_tool_setup
|
|
329
|
+
)
|
|
331
330
|
if not config:
|
|
332
331
|
util.error(f'eda.py main: problem loading config, {args=}')
|
|
333
332
|
return 3
|
|
334
333
|
|
|
335
|
-
|
|
336
|
-
util.debug(f'eda process_tokens: {parsed=} {unparsed=}')
|
|
337
|
-
for value in unparsed:
|
|
338
|
-
if value in config['command_handler'].keys():
|
|
339
|
-
command = value
|
|
340
|
-
unparsed.remove(value)
|
|
341
|
-
break
|
|
342
|
-
|
|
343
334
|
# Deal with help, now that we have the command (if it was set).
|
|
344
335
|
if parsed.help:
|
|
345
336
|
if not command:
|
|
@@ -363,7 +354,9 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
363
354
|
util.debug(f'{command=}')
|
|
364
355
|
util.debug(f'{sco.config=}')
|
|
365
356
|
util.debug(f'{type(sco)=}')
|
|
366
|
-
if not parsed.tool and
|
|
357
|
+
if not parsed.tool and \
|
|
358
|
+
command not in config.get('command_determines_tool', []) and \
|
|
359
|
+
command not in config.get('command_uses_no_tools', []):
|
|
367
360
|
use_tool = which_tool(command, config)
|
|
368
361
|
util.info(f"--tool not specified, using default for {command=}: {use_tool}")
|
|
369
362
|
|
|
@@ -377,7 +370,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
|
|
|
377
370
|
sco.config['eda_original_args'] = original_args
|
|
378
371
|
|
|
379
372
|
setattr(sco, 'command_name', command) # as a safeguard, b/c 'command' is not always passed to 'sco'
|
|
380
|
-
unparsed = sco.process_tokens(deferred_tokens)
|
|
373
|
+
unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
|
|
381
374
|
|
|
382
375
|
# query the status from the Command object (0 is pass, > 0 is fail)
|
|
383
376
|
rc = getattr(sco, 'status', 1)
|
|
@@ -439,7 +432,6 @@ def main(*args):
|
|
|
439
432
|
# Handle --config-yml= arg
|
|
440
433
|
config, unparsed = eda_config.get_eda_config(unparsed)
|
|
441
434
|
|
|
442
|
-
|
|
443
435
|
# Note - we used to call: config = init_config(config=config)
|
|
444
436
|
# However, we now defer calling init_config(..) until eda.process_tokens(..)
|
|
445
437
|
|
|
@@ -459,22 +451,8 @@ def main(*args):
|
|
|
459
451
|
config=config)
|
|
460
452
|
|
|
461
453
|
|
|
462
|
-
def main_cli(
|
|
454
|
+
def main_cli() -> None:
|
|
463
455
|
''' Returns None, will exit with return code. Entry point for package script or __main__.'''
|
|
464
|
-
|
|
465
|
-
if support_respawn and '--no-respawn' not in sys.argv:
|
|
466
|
-
# If someone called eda.py directly (aka, __name__ == '__main__'),
|
|
467
|
-
# then we still support a legacy mode of operation - where we check
|
|
468
|
-
# for OC_ROOT (in env, or git repo) to make sure this is the right
|
|
469
|
-
# location of eda.py by calling main_cli(support_respawn=True).
|
|
470
|
-
# Otherwise, we do not respawn $OC_ROOT/bin/eda.py
|
|
471
|
-
# Can also be avoided with --no-respawn.
|
|
472
|
-
|
|
473
|
-
# Note - respawn will never work if calling as a package executable script,
|
|
474
|
-
# which is why our package entrypoint will be main_cli() w/out support_respawn.
|
|
475
|
-
main_maybe_respawn()
|
|
476
|
-
|
|
477
|
-
|
|
478
456
|
signal.signal(signal.SIGINT, signal_handler)
|
|
479
457
|
util.global_exit_allowed = True
|
|
480
458
|
# Strip eda or eda.py from sys.argv, we know who we are if called from __main__:
|
|
@@ -482,46 +460,8 @@ def main_cli(support_respawn=False):
|
|
|
482
460
|
util.exit(rc)
|
|
483
461
|
|
|
484
462
|
|
|
485
|
-
def main_maybe_respawn():
|
|
486
|
-
''' Returns None, will respawn - run - exit, or will return and the command
|
|
487
|
-
|
|
488
|
-
is expected to run in main_cli()'''
|
|
489
|
-
|
|
490
|
-
# First we check if we are respawning
|
|
491
|
-
this_path = os.path.realpath(__file__)
|
|
492
|
-
if debug_respawn: util.info(f"RESPAWN: this_path : '{this_path}'")
|
|
493
|
-
oc_root = util.get_oc_root()
|
|
494
|
-
if debug_respawn: util.info(f"RESPAWN: oc_root : '{oc_root}'")
|
|
495
|
-
cwd = util.getcwd()
|
|
496
|
-
if debug_respawn: util.info(f"RESPAWN: cwd : '{cwd}'")
|
|
497
|
-
if oc_root:
|
|
498
|
-
new_paths = [
|
|
499
|
-
os.path.join(oc_root, 'opencos', 'eda.py'),
|
|
500
|
-
os.path.join(oc_root, 'bin', 'eda'),
|
|
501
|
-
]
|
|
502
|
-
if debug_respawn: util.info(f"RESPAWN: {new_paths=} {this_path=}")
|
|
503
|
-
if this_path not in new_paths and os.path.exists(new_paths[0]):
|
|
504
|
-
# we are not the correct version of EDA for this Git repo, we should respawn
|
|
505
|
-
util.info(f"{this_path} respawning {new_paths[0]} in {cwd} with --no-respawn")
|
|
506
|
-
sys.argv[0] = new_paths[0]
|
|
507
|
-
sys.argv.insert(1, '--no-respawn')
|
|
508
|
-
proc = subprocess.Popen(sys.argv, shell=0, cwd=cwd, universal_newlines=True)
|
|
509
|
-
while True:
|
|
510
|
-
try:
|
|
511
|
-
proc.communicate()
|
|
512
|
-
break
|
|
513
|
-
except KeyboardInterrupt:
|
|
514
|
-
continue
|
|
515
|
-
# get exit status from proc and return it
|
|
516
|
-
util.exit(proc.returncode, quiet=True)
|
|
517
|
-
else:
|
|
518
|
-
if debug_respawn: util.info(f"RESPAWN: {oc_root=} respawn not necessary")
|
|
519
|
-
else:
|
|
520
|
-
if debug_respawn: util.info("RESPAWN: respawn not necessary")
|
|
521
|
-
|
|
522
|
-
|
|
523
463
|
if __name__ == '__main__':
|
|
524
|
-
main_cli(
|
|
464
|
+
main_cli()
|
|
525
465
|
|
|
526
466
|
# IDEAS:
|
|
527
467
|
# * options with no default (i.e. if user doesn't override, THEN we set it, like "seed" or "work-dir") can be given a
|
opencos/eda_base.py
CHANGED
|
@@ -11,6 +11,7 @@ import signal
|
|
|
11
11
|
import sys
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
import opencos
|
|
16
17
|
from opencos import seed, deps_helpers, util, files
|
|
@@ -184,20 +185,24 @@ class Command:
|
|
|
184
185
|
'export-json': False, # generates an export.json suitable for a testrunner, if possible for self.command.
|
|
185
186
|
'enable-tags': [],
|
|
186
187
|
'disable-tags': [],
|
|
188
|
+
'test-mode': False,
|
|
187
189
|
})
|
|
188
190
|
self.args_help.update({
|
|
189
|
-
'stop-before-compile': 'stop this run before any compile (if possible for tool) and'
|
|
190
|
-
|
|
191
|
+
'stop-before-compile': ('stop this run before any compile (if possible for tool) and'
|
|
192
|
+
' save .sh scripts in eda-dir/'),
|
|
191
193
|
'eda-dir': 'relative directory where eda logs are saved',
|
|
192
194
|
'export': 'export results for these targets in eda-dir',
|
|
193
195
|
'export-run': 'export, and run, results for these targets in eda-dir',
|
|
194
196
|
'export-json': 'export, and save a JSON file per target',
|
|
195
|
-
'work-dir': 'Optional override for working directory, often defaults to
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
'work-dir': ('Optional override for working directory, often defaults to'
|
|
198
|
+
' ./eda.work/<top>.<command>'),
|
|
199
|
+
'enable-tags': ('DEPS markup tag names to be force enabled for this'
|
|
200
|
+
' command (mulitple appends to list).'),
|
|
201
|
+
'diable-tags': ('DEPS markup tag names to be disabled (even if they'
|
|
202
|
+
' match the criteria) for this command (mulitple appends to list).'
|
|
203
|
+
' --disable-tags has higher precedence than --enable-tags.'),
|
|
204
|
+
'test-mode': ('command and tool dependent, usually stops the command early without'
|
|
205
|
+
' executing.'),
|
|
201
206
|
})
|
|
202
207
|
self.modified_args = {}
|
|
203
208
|
self.config = copy.deepcopy(config) # avoid external modifications.
|
|
@@ -223,8 +228,8 @@ class Command:
|
|
|
223
228
|
|
|
224
229
|
def create_work_dir(self):
|
|
225
230
|
util.debug(f"create_work_dir: {self.args['eda-dir']=} {self.args['work-dir']=}")
|
|
226
|
-
if
|
|
227
|
-
|
|
231
|
+
if not os.path.exists(self.args['eda-dir']):
|
|
232
|
+
util.safe_mkdir(self.args['eda-dir'])
|
|
228
233
|
util.info(f"create_work_dir: created {self.args['eda-dir']}")
|
|
229
234
|
if self.args['design'] == "":
|
|
230
235
|
if ('top' in self.args) and (self.args['top'] != ""):
|
|
@@ -250,14 +255,35 @@ class Command:
|
|
|
250
255
|
if (os.path.exists(self.args['work-dir'])):
|
|
251
256
|
if os.path.exists(keep_file) and not self.args['force']:
|
|
252
257
|
self.error(f"Cannot remove old work dir due to '{keep_file}'")
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
elif os.path.abspath(self.args['work-dir']) in os.getcwd():
|
|
259
|
+
# This effectively checks if
|
|
260
|
+
# --work-dir=.
|
|
261
|
+
# --work-dir=$PWD
|
|
262
|
+
# --work-dir=/some/path/almost/here
|
|
263
|
+
# Allow it, but preserve the existing directory, we don't want to blow away
|
|
264
|
+
# files up-heir from us.
|
|
265
|
+
# Enables support for --work-dir=.
|
|
266
|
+
util.info(f"Not removing existing work-dir: '{self.args['work-dir']}' is within {os.getcwd()=}")
|
|
267
|
+
elif str(Path(self.args['work-dir'])).startswith(str(Path('/'))):
|
|
268
|
+
# Do not allow other absolute path work dirs if it already exists.
|
|
269
|
+
# This prevents you from --work-dir=~ and eda wipes out your home dir.
|
|
270
|
+
self.error(f'Cannot use work-dir={self.args["work-dir"]} starting with absolute path "/"')
|
|
271
|
+
elif str(Path('..')) in str(Path(self.args['work-dir'])):
|
|
272
|
+
# Do not allow other ../ work dirs if it already exists.
|
|
273
|
+
self.error(f'Cannot use work-dir={self.args["work-dir"]} with up-hierarchy ../ paths')
|
|
274
|
+
else:
|
|
275
|
+
# If we made it this far, on a directory that exists, that appears safe
|
|
276
|
+
# to delete and re-create:
|
|
277
|
+
util.info(f"Removing previous '{self.args['work-dir']}'")
|
|
278
|
+
shutil.rmtree(self.args['work-dir'])
|
|
279
|
+
util.safe_mkdir(self.args['work-dir'])
|
|
280
|
+
util.debug(f'create_work_dir: created {self.args["work-dir"]}')
|
|
281
|
+
else:
|
|
282
|
+
util.safe_mkdir(self.args['work-dir'])
|
|
283
|
+
util.debug(f'create_work_dir: created {self.args["work-dir"]}')
|
|
257
284
|
if (self.args['keep']):
|
|
258
285
|
open(keep_file, 'w').close()
|
|
259
|
-
util.debug(f'create_work_dir: created {keep_file}')
|
|
260
|
-
util.info(f'create_work_dir: created {self.args["work-dir"]}')
|
|
286
|
+
util.debug(f'create_work_dir: created {keep_file=}')
|
|
261
287
|
return self.args['work-dir']
|
|
262
288
|
|
|
263
289
|
def exec(self, work_dir, command_list, background=False, stop_on_error=True,
|
|
@@ -550,7 +576,7 @@ class Command:
|
|
|
550
576
|
self.set_tool_defines()
|
|
551
577
|
|
|
552
578
|
|
|
553
|
-
def help(self, tokens: list = []):
|
|
579
|
+
def help(self, tokens: list = []) -> None:
|
|
554
580
|
'''Since we don't quite follow standard argparger help()/usage(), we'll format our own
|
|
555
581
|
|
|
556
582
|
if self.args_help has additional help information.
|
|
@@ -569,8 +595,13 @@ class Command:
|
|
|
569
595
|
|
|
570
596
|
print_base_help()
|
|
571
597
|
lines = []
|
|
598
|
+
if not self.args:
|
|
599
|
+
print(f'Unparsed args: {tokens}')
|
|
600
|
+
return
|
|
601
|
+
|
|
572
602
|
if self.command_name:
|
|
573
|
-
lines.append(f"Generic help for command='{self.command_name}'
|
|
603
|
+
lines.append(f"Generic help for command='{self.command_name}'"
|
|
604
|
+
f" (using '{self.__class__.__name__}')")
|
|
574
605
|
else:
|
|
575
606
|
lines.append(f"Generic help (from class Command):")
|
|
576
607
|
|
|
@@ -752,7 +783,7 @@ class CommandDesign(Command):
|
|
|
752
783
|
# token = '\'+define+OC_ROOT="/foo/bar/opencos"\''
|
|
753
784
|
# So we strip all outer ' or " on the plusarg:
|
|
754
785
|
plusarg = util.strip_outer_quotes(plusarg)
|
|
755
|
-
if pwd
|
|
786
|
+
if not pwd:
|
|
756
787
|
pwd = ''
|
|
757
788
|
|
|
758
789
|
if plusarg.startswith('+define+'):
|
|
@@ -1356,6 +1387,8 @@ class CommandParallel(Command):
|
|
|
1356
1387
|
fancy_mode = util.args['fancy'] and (num_parallel > 1) and (num_parallel <= (lines-6))
|
|
1357
1388
|
multi_cwd = util.getcwd() + os.sep
|
|
1358
1389
|
|
|
1390
|
+
self.patch_jobs_for_duplicate_target_names()
|
|
1391
|
+
|
|
1359
1392
|
if run_parallel:
|
|
1360
1393
|
# we are doing this multi-threaded
|
|
1361
1394
|
util.info(f"Parallel: Running multi-threaded, starting {num_parallel} workers")
|
|
@@ -1514,6 +1547,10 @@ class CommandParallel(Command):
|
|
|
1514
1547
|
self.status = 0 if len(self.jobs_status) == 0 else max(self.jobs_status)
|
|
1515
1548
|
util.fancy_stop()
|
|
1516
1549
|
|
|
1550
|
+
@staticmethod
|
|
1551
|
+
def get_name_from_target(target: str) -> str:
|
|
1552
|
+
return target.replace('../', '').lstrip('./')
|
|
1553
|
+
|
|
1517
1554
|
|
|
1518
1555
|
def update_args_list(self, args: list, tool: str) -> None:
|
|
1519
1556
|
'''Modfies list args, using allow-listed known top-level args:
|
|
@@ -1567,3 +1604,58 @@ class CommandParallel(Command):
|
|
|
1567
1604
|
# Remove unparsed args starting with '+', since those are commonly sent downstream to
|
|
1568
1605
|
# single job (example, CommandSim plusargs).
|
|
1569
1606
|
return [x for x in single_cmd_unparsed if not x.startswith('+')]
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def patch_jobs_for_duplicate_target_names(self) -> None:
|
|
1610
|
+
'''Examines list self.jobs, and if leaf target names are duplicate will
|
|
1611
|
+
patch each command's job-name to:
|
|
1612
|
+
--job-name=path.leaf.command[.tool]
|
|
1613
|
+
'''
|
|
1614
|
+
|
|
1615
|
+
def get_job_name(job_dict: dict) -> str:
|
|
1616
|
+
'''Fishes the job-name out of an entry in self.jobs'''
|
|
1617
|
+
for i, item in enumerate(job_dict['command_list']):
|
|
1618
|
+
if item.startswith('--job-name='):
|
|
1619
|
+
_, name = item.split('--job-name=')
|
|
1620
|
+
return name
|
|
1621
|
+
elif item == '--job-name':
|
|
1622
|
+
return job_dict['command_list'][i + 1]
|
|
1623
|
+
return ''
|
|
1624
|
+
|
|
1625
|
+
def replace_job_name(job_dict: dict, new_job_name: str) -> dict:
|
|
1626
|
+
'''Replaces the job-name in an entry in self.jobs'''
|
|
1627
|
+
for i, item in enumerate(job_dict['command_list']):
|
|
1628
|
+
if item.startswith('--job-name='):
|
|
1629
|
+
job_dict['command_list'][i] = '--job-name=' + new_job_name
|
|
1630
|
+
return job_dict
|
|
1631
|
+
elif item == '--job-name':
|
|
1632
|
+
job_dict['command_list'][i + 1] = new_job_name
|
|
1633
|
+
return job_dict
|
|
1634
|
+
return job_dict
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
job_names_count_dict = {}
|
|
1638
|
+
for job_dict in self.jobs:
|
|
1639
|
+
|
|
1640
|
+
key = get_job_name(job_dict)
|
|
1641
|
+
if not key:
|
|
1642
|
+
self.error(f'{job_dict=} needs to have a --job-name= arg attached')
|
|
1643
|
+
if key not in job_names_count_dict:
|
|
1644
|
+
job_names_count_dict[key] = 1
|
|
1645
|
+
else:
|
|
1646
|
+
job_names_count_dict[key] += 1
|
|
1647
|
+
|
|
1648
|
+
for i, job_dict in enumerate(self.jobs):
|
|
1649
|
+
key = get_job_name(job_dict)
|
|
1650
|
+
if job_names_count_dict[key] < 2:
|
|
1651
|
+
continue
|
|
1652
|
+
|
|
1653
|
+
tpath, _ = os.path.split(job_dict['target'])
|
|
1654
|
+
|
|
1655
|
+
# prepend path information to job-name:
|
|
1656
|
+
patched_sub_work_dir = False
|
|
1657
|
+
patched_target_path = os.path.relpath(tpath).replace(os.sep, '_')
|
|
1658
|
+
new_job_name = f'{patched_target_path}.{key}'
|
|
1659
|
+
new_job_dict = replace_job_name(job_dict, new_job_name)
|
|
1660
|
+
self.jobs[i] = new_job_dict
|
|
1661
|
+
util.debug(f'Patched job {job_dict["name"]}: --job-name={new_job_name}')
|
opencos/eda_config.py
CHANGED
|
@@ -15,9 +15,11 @@ class Defaults:
|
|
|
15
15
|
home_override_config_yml = os.path.join(
|
|
16
16
|
os.environ.get('HOME', ''), '.opencos-eda', 'EDA_CONFIG.yml'
|
|
17
17
|
)
|
|
18
|
-
|
|
18
|
+
opencos_config_yml = 'eda_config_defaults.yml'
|
|
19
|
+
config_yml = ''
|
|
19
20
|
|
|
20
21
|
supported_config_keys = set([
|
|
22
|
+
'DEFAULT_HANDLERS',
|
|
21
23
|
'defines',
|
|
22
24
|
'dep_command_enables',
|
|
23
25
|
'dep_tags_enables',
|
|
@@ -26,10 +28,11 @@ class Defaults:
|
|
|
26
28
|
'bare_plusarg_supported',
|
|
27
29
|
'dep_sub',
|
|
28
30
|
'vars',
|
|
29
|
-
'tools',
|
|
30
|
-
'auto_tools_order',
|
|
31
31
|
'file_extensions',
|
|
32
32
|
'command_determines_tool',
|
|
33
|
+
'command_uses_no_tools',
|
|
34
|
+
'tools',
|
|
35
|
+
'auto_tools_order',
|
|
33
36
|
])
|
|
34
37
|
supported_config_auto_tools_order_keys = set([
|
|
35
38
|
'exe', 'handlers', 'requires_env', 'requires_py', 'requires_cmd',
|
|
@@ -53,6 +56,14 @@ class Defaults:
|
|
|
53
56
|
])
|
|
54
57
|
|
|
55
58
|
|
|
59
|
+
if os.path.exists(Defaults.environ_override_config_yml):
|
|
60
|
+
Defaults.config_yml = Defaults.environ_override_config_yml
|
|
61
|
+
elif os.path.exists(Defaults.home_override_config_yml):
|
|
62
|
+
Defaults.config_yml = Defaults.home_override_config_yml
|
|
63
|
+
else:
|
|
64
|
+
Defaults.config_yml = Defaults.opencos_config_yml
|
|
65
|
+
|
|
66
|
+
|
|
56
67
|
def find_eda_config_yml_fpath(filename:str, package_search_only=False, package_search_enabled=True) -> str:
|
|
57
68
|
'''Locates the filename (.yml) either from fullpath provided or from the sys.path
|
|
58
69
|
opencos package paths.'''
|
|
@@ -192,15 +203,9 @@ def get_config_merged_with_defaults(config:dict) -> dict:
|
|
|
192
203
|
return default_config
|
|
193
204
|
|
|
194
205
|
def get_argparser() -> argparse.ArgumentParser:
|
|
195
|
-
if os.path.exists(Defaults.environ_override_config_yml):
|
|
196
|
-
default_config_yml = Defaults.environ_override_config_yml
|
|
197
|
-
elif os.path.exists(Defaults.home_override_config_yml):
|
|
198
|
-
default_config_yml = Defaults.home_override_config_yml
|
|
199
|
-
else:
|
|
200
|
-
default_config_yml = Defaults.config_yml
|
|
201
206
|
parser = argparse.ArgumentParser(prog='opencos eda config options', add_help=False, allow_abbrev=False)
|
|
202
|
-
parser.add_argument('--config-yml', type=str, default=
|
|
203
|
-
help=f'YAML filename to use for configuration (default {
|
|
207
|
+
parser.add_argument('--config-yml', type=str, default=Defaults.config_yml,
|
|
208
|
+
help=f'YAML filename to use for configuration (default {Defaults.config_yml})')
|
|
204
209
|
return parser
|
|
205
210
|
|
|
206
211
|
def get_argparser_short_help() -> str:
|
|
@@ -213,6 +218,8 @@ def get_eda_config(args:list, quiet=False) -> (dict, list):
|
|
|
213
218
|
|
|
214
219
|
Handles args for:
|
|
215
220
|
--config-yml=<YAMLFILE>
|
|
221
|
+
|
|
222
|
+
This will merge the result with the default config (if overriden)
|
|
216
223
|
'''
|
|
217
224
|
|
|
218
225
|
parser = get_argparser()
|
|
@@ -237,5 +244,7 @@ def get_eda_config(args:list, quiet=False) -> (dict, list):
|
|
|
237
244
|
else:
|
|
238
245
|
config = None
|
|
239
246
|
|
|
247
|
+
if parsed.config_yml != Defaults.config_yml:
|
|
248
|
+
config = get_config_merged_with_defaults(config)
|
|
240
249
|
|
|
241
250
|
return config, unparsed
|