opencos-eda 0.3.2__py3-none-any.whl → 0.3.5__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/deps/defaults.py +1 -0
- opencos/deps/deps_processor.py +47 -22
- opencos/deps_schema.py +17 -0
- opencos/eda.py +7 -14
- opencos/eda_base.py +1 -0
- opencos/eda_config.py +84 -40
- opencos/eda_config_defaults.yml +5 -1
- opencos/eda_extract_targets.py +0 -0
- opencos/eda_tool_helper.py +19 -0
- opencos/hw/oc_cli.py +0 -0
- opencos/tests/helpers.py +58 -2
- opencos/tests/test_eda.py +11 -1
- opencos/tools/iverilog.py +4 -0
- opencos/tools/riviera.py +23 -12
- opencos/tools/slang.py +6 -0
- opencos/tools/verilator.py +133 -38
- opencos/tools/vivado.py +1 -0
- opencos/util.py +22 -14
- opencos/utils/str_helpers.py +6 -1
- opencos/utils/subprocess_helpers.py +66 -3
- {opencos_eda-0.3.2.dist-info → opencos_eda-0.3.5.dist-info}/METADATA +11 -2
- {opencos_eda-0.3.2.dist-info → opencos_eda-0.3.5.dist-info}/RECORD +25 -25
- {opencos_eda-0.3.2.dist-info → opencos_eda-0.3.5.dist-info}/WHEEL +0 -0
- {opencos_eda-0.3.2.dist-info → opencos_eda-0.3.5.dist-info}/entry_points.txt +0 -0
- {opencos_eda-0.3.2.dist-info → opencos_eda-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.3.2.dist-info → opencos_eda-0.3.5.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.3.2.dist-info → opencos_eda-0.3.5.dist-info}/top_level.txt +0 -0
opencos/tools/riviera.py
CHANGED
|
@@ -13,6 +13,7 @@ import subprocess
|
|
|
13
13
|
from opencos import util
|
|
14
14
|
from opencos.tools.modelsim_ase import ToolModelsimAse, CommandSimModelsimAse
|
|
15
15
|
from opencos.utils.str_helpers import sanitize_defines_for_sh
|
|
16
|
+
from opencos.utils import status_constants
|
|
16
17
|
|
|
17
18
|
class ToolRiviera(ToolModelsimAse):
|
|
18
19
|
'''ToolRiviera used by opencos.eda for --tool=riviera'''
|
|
@@ -61,6 +62,7 @@ class CommandSimRiviera(CommandSimModelsimAse, ToolRiviera):
|
|
|
61
62
|
'gui': False,
|
|
62
63
|
'waves-fst': True,
|
|
63
64
|
'waves-vcd': False,
|
|
65
|
+
'coverage-tcl': '',
|
|
64
66
|
})
|
|
65
67
|
self.args_help.update({
|
|
66
68
|
'waves-fst': (
|
|
@@ -70,6 +72,10 @@ class CommandSimRiviera(CommandSimModelsimAse, ToolRiviera):
|
|
|
70
72
|
),
|
|
71
73
|
'waves-vcd': 'If using --waves, apply simulation runtime arg +trace=vcd',
|
|
72
74
|
'waves': 'Save a .asdb offline wavefile, can be used with --waves-fst or --waves-vcd',
|
|
75
|
+
'coverage-tcl': (
|
|
76
|
+
'bring your own .tcl file to run in Riviera (vsim) for coverage. The default'
|
|
77
|
+
' tcl steps are (from tool config in --config-yml): '
|
|
78
|
+
) + '; '.join(self.tool_config.get('simulate-coverage-tcl', [])),
|
|
73
79
|
})
|
|
74
80
|
|
|
75
81
|
def set_tool_defines(self):
|
|
@@ -183,7 +189,9 @@ class CommandSimRiviera(CommandSimModelsimAse, ToolRiviera):
|
|
|
183
189
|
|
|
184
190
|
|
|
185
191
|
|
|
186
|
-
def write_vsim_dot_do(
|
|
192
|
+
def write_vsim_dot_do( # pylint: disable=too-many-branches
|
|
193
|
+
self, dot_do_to_write: list
|
|
194
|
+
) -> None:
|
|
187
195
|
'''Writes files(s) based on dot_do_to_write(list of str)
|
|
188
196
|
|
|
189
197
|
list arg values can be empty (all) or have items 'all', 'sim', 'lint', 'vlog'.'''
|
|
@@ -278,18 +286,21 @@ class CommandSimRiviera(CommandSimModelsimAse, ToolRiviera):
|
|
|
278
286
|
"}",
|
|
279
287
|
]
|
|
280
288
|
|
|
289
|
+
vsim_dot_do_lines += [
|
|
290
|
+
"run -all;",
|
|
291
|
+
]
|
|
281
292
|
if self.args['coverage']:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
if self.args['coverage-tcl']:
|
|
294
|
+
tclfile = os.path.abspath(self.args['coverage-tcl'])
|
|
295
|
+
if not os.path.isfile(tclfile):
|
|
296
|
+
self.error(f'--coverage-tcl file not found: {tclfile}',
|
|
297
|
+
error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
|
|
298
|
+
vsim_dot_do_lines += [
|
|
299
|
+
f'source {tclfile}'
|
|
300
|
+
]
|
|
301
|
+
else:
|
|
302
|
+
# default TCL for coverage:
|
|
303
|
+
vsim_dot_do_lines += self.tool_config.get('simulate-coverage-tcl', [])
|
|
293
304
|
|
|
294
305
|
|
|
295
306
|
vsim_dot_do_lines += [
|
opencos/tools/slang.py
CHANGED
|
@@ -128,6 +128,7 @@ class CommandElabSlang(CommandElab, ToolSlang):
|
|
|
128
128
|
command_list += self.tool_config.get('compile-args', '--single-unit').split()
|
|
129
129
|
command_list += self.args['slang-args'] # add user args.
|
|
130
130
|
command_list += self._get_slang_json_args(command_exe=command_list[0])
|
|
131
|
+
command_list += self._get_slang_tool_config_waivers()
|
|
131
132
|
|
|
132
133
|
# incdirs
|
|
133
134
|
for value in self.incdirs:
|
|
@@ -214,6 +215,11 @@ class CommandElabSlang(CommandElab, ToolSlang):
|
|
|
214
215
|
|
|
215
216
|
return command_list
|
|
216
217
|
|
|
218
|
+
def _get_slang_tool_config_waivers(self) -> list:
|
|
219
|
+
# Add compile waivers from config and command-line args
|
|
220
|
+
return [f'-Wno-{waiver}' for waiver in
|
|
221
|
+
self.tool_config.get('compile-waivers', []) + self.args['compile-waivers']]
|
|
222
|
+
|
|
217
223
|
|
|
218
224
|
class CommandLintSlang(CommandElabSlang):
|
|
219
225
|
'''CommandLintSlang is a command handler for: eda lint --tool=slang.'''
|
opencos/tools/verilator.py
CHANGED
|
@@ -5,10 +5,12 @@ Contains classes for ToolVerilator and VerilatorSim, VerilatorElab.
|
|
|
5
5
|
|
|
6
6
|
# pylint: disable=R0801 # (calling functions with same arguments)
|
|
7
7
|
|
|
8
|
+
import multiprocessing
|
|
8
9
|
import os
|
|
9
10
|
import shutil
|
|
10
11
|
import subprocess
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
from opencos import util
|
|
13
15
|
from opencos.eda_base import Tool
|
|
14
16
|
from opencos.commands import CommandSim
|
|
@@ -81,6 +83,7 @@ class VerilatorSim(CommandSim, ToolVerilator):
|
|
|
81
83
|
'lint-only': False,
|
|
82
84
|
'cc-mode': False,
|
|
83
85
|
'verilator-coverage-args': [],
|
|
86
|
+
'uvm': False,
|
|
84
87
|
'x-assign': '',
|
|
85
88
|
'x-initial': '',
|
|
86
89
|
})
|
|
@@ -114,6 +117,15 @@ class VerilatorSim(CommandSim, ToolVerilator):
|
|
|
114
117
|
' where valid string values are: 0, unique, fast.'
|
|
115
118
|
' Also conditinally adds to verilated exe call:'
|
|
116
119
|
' +verilator+rand+reset+[0,2] for arg values 0, unique|fast'),
|
|
120
|
+
'uvm': (
|
|
121
|
+
'Warns on Verilator < 5.042, or missing $UVM_HOME environment var set (or in'
|
|
122
|
+
' .env, $UVM_HOME/uvm_pkg.sv should exist), and will run verilator with args:'
|
|
123
|
+
' -Wno-fatal +define+UVM_NO_DPI'
|
|
124
|
+
),
|
|
125
|
+
'verilator-coverage-args': (
|
|
126
|
+
'Requires --coverage, args to be applied to verilator_coverage, which runs'
|
|
127
|
+
' after running the compiled executable simulation'
|
|
128
|
+
),
|
|
117
129
|
})
|
|
118
130
|
|
|
119
131
|
self.verilate_command_lists = []
|
|
@@ -210,18 +222,19 @@ class VerilatorSim(CommandSim, ToolVerilator):
|
|
|
210
222
|
|
|
211
223
|
verilate_command_list = self._get_start_verilator_command_list(lint_only=lint_only)
|
|
212
224
|
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
225
|
+
# Handle UVM things (return args), but also handles uvm_pkg.sv in self.files_sv:
|
|
226
|
+
# since we run this 2x (lint-only and normal) only do warnings for one of them:
|
|
227
|
+
verilate_command_list += self._verilator_args_uvm(
|
|
228
|
+
warnings=(not lint_only), add_uvm_pkg_if_found=True
|
|
229
|
+
)
|
|
218
230
|
|
|
219
231
|
verilate_command_list += self._get_verilator_tool_config_waivers()
|
|
220
232
|
|
|
221
|
-
verilate_command_list += self.
|
|
233
|
+
verilate_command_list += self._verilator_args_defaults_cflags_nproc()
|
|
222
234
|
|
|
223
235
|
verilate_command_list += self._get_verilator_waves_args(lint_only=lint_only)
|
|
224
236
|
|
|
237
|
+
|
|
225
238
|
if self.args.get('coverage', True):
|
|
226
239
|
verilate_command_list += self.tool_config.get(
|
|
227
240
|
'compile-coverage-args', '--coverage').split()
|
|
@@ -425,45 +438,51 @@ class VerilatorSim(CommandSim, ToolVerilator):
|
|
|
425
438
|
return ret
|
|
426
439
|
|
|
427
440
|
|
|
428
|
-
def
|
|
441
|
+
def _verilator_args_defaults_cflags_nproc(self) -> list:
|
|
429
442
|
'''Returns list of args to be added to verilator (compile) step
|
|
430
443
|
|
|
431
|
-
Uses self.args['verilate-args']
|
|
444
|
+
Uses self.args['verilate-args'], self.args['compile-args'], and self.tool_config
|
|
445
|
+
|
|
446
|
+
Sets -j <value> and -CFLAGS -O<value> if not present in --config-yml, --compile-args,
|
|
447
|
+
or --verilate-args. If present, chooses the first instance (does not present duplicates
|
|
448
|
+
to verilator call).
|
|
432
449
|
'''
|
|
433
450
|
|
|
434
|
-
# We can only support one -CFLAGS followed by one -O[0-9] arg in
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
451
|
+
# We can only support one -CFLAGS followed by one -O[0-9] arg in
|
|
452
|
+
# --verilate-args or --compile-args.
|
|
453
|
+
|
|
454
|
+
# Add compile args from our self.config (tools.verilator.compile-args str)
|
|
455
|
+
verilate_args = self.args['verilate-args'] + \
|
|
456
|
+
self.args['compile-args'] + \
|
|
457
|
+
self.tool_config.get(
|
|
458
|
+
'compile-args',
|
|
459
|
+
'--timing --assert --autoflush -sv').split()
|
|
460
|
+
|
|
438
461
|
util.debug(f"{self.args['verilate-args']=}")
|
|
439
462
|
util.debug(f"{self.args['compile-args']=}")
|
|
440
|
-
for arg in self.args['verilate-args'] + self.args['compile-args']:
|
|
441
|
-
# pick the first ones we see of these:
|
|
442
|
-
if arg == '-CFLAGS':
|
|
443
|
-
prev_arg_is_cflags = True
|
|
444
|
-
if arg not in verilate_cflags_args_dict:
|
|
445
|
-
# We can only have 1
|
|
446
|
-
verilate_cflags_args_dict[arg] = True
|
|
447
|
-
verilate_args.append(arg)
|
|
448
|
-
else:
|
|
449
|
-
util.debug(f'Previous saw -CFLAGS args {verilate_cflags_args_dict=},',
|
|
450
|
-
f'skipping new {arg=}')
|
|
451
|
-
|
|
452
|
-
elif arg.startswith('-O') and len(arg) == 3:
|
|
453
|
-
if '-O' not in verilate_cflags_args_dict and prev_arg_is_cflags:
|
|
454
|
-
# We can only have 1
|
|
455
|
-
verilate_cflags_args_dict['-O'] = arg[-1]
|
|
456
|
-
verilate_args.append(arg)
|
|
457
|
-
else:
|
|
458
|
-
util.debug(f'Previous saw -CFLAGS args {verilate_cflags_args_dict=},',
|
|
459
|
-
f'skipping new {arg=}')
|
|
460
|
-
prev_arg_is_cflags = False
|
|
461
|
-
|
|
462
|
-
else:
|
|
463
|
-
prev_arg_is_cflags = False
|
|
464
|
-
verilate_args.append(arg)
|
|
465
463
|
|
|
466
|
-
|
|
464
|
+
dash_j_arg_indices = []
|
|
465
|
+
cflags_dasho_args_indices = []
|
|
466
|
+
for i, arg in enumerate(list(verilate_args)):
|
|
467
|
+
# There can only be one of these: -j <value>, similarly can only be one of
|
|
468
|
+
# -CFLAGS -O<value>
|
|
469
|
+
if (i + 1) < len(verilate_args):
|
|
470
|
+
if arg == '-j':
|
|
471
|
+
dash_j_arg_indices.extend([i, i + 1])
|
|
472
|
+
if arg == '-CFLAGS':
|
|
473
|
+
next_arg = verilate_args[i + 1]
|
|
474
|
+
if next_arg.startswith('-O') and len(next_arg) == 3:
|
|
475
|
+
cflags_dasho_args_indices.extend([i, i + 1])
|
|
476
|
+
|
|
477
|
+
# For -j <value> we'll pick the first one, remove the rest.
|
|
478
|
+
# Same goes for -CFLAGS -O<value>
|
|
479
|
+
for index in dash_j_arg_indices[2:] + cflags_dasho_args_indices[2:]:
|
|
480
|
+
verilate_args[index] = ''
|
|
481
|
+
|
|
482
|
+
verilate_args = [x for x in verilate_args if x != ''] # strip empty str.
|
|
483
|
+
|
|
484
|
+
# Support for --optimize which will use -CFLAGS -O3, if -CFLAGS is not present at all.
|
|
485
|
+
if cflags_dasho_args_indices:
|
|
467
486
|
# add whatever args were passed via 'compile-args' or 'verilate_args'. Note these will
|
|
468
487
|
# take precedence over the --optimize arg.
|
|
469
488
|
pass
|
|
@@ -472,11 +491,87 @@ class VerilatorSim(CommandSim, ToolVerilator):
|
|
|
472
491
|
# (slower compile, better runtime)
|
|
473
492
|
verilate_args += '-CFLAGS', '-O3'
|
|
474
493
|
else:
|
|
494
|
+
# Default to -O1:
|
|
475
495
|
verilate_args += '-CFLAGS', '-O1'
|
|
476
496
|
|
|
497
|
+
# If there was no -j setting, then use max(2, $(nproc) - 1)
|
|
498
|
+
if not dash_j_arg_indices:
|
|
499
|
+
nproc = max(2, multiprocessing.cpu_count() - 1)
|
|
500
|
+
verilate_args += '-j', f'{nproc}'
|
|
501
|
+
|
|
502
|
+
|
|
477
503
|
return verilate_args
|
|
478
504
|
|
|
479
505
|
|
|
506
|
+
def _verilator_support_uvm_pkg_fpath(self, add_if_found: bool = True) -> bool:
|
|
507
|
+
'''Returns False if we could not find a suitable uvm_pkg.sv to use, or if --no-uvm.
|
|
508
|
+
|
|
509
|
+
This will also auto-add uvm_pkg.sv from $UVM_HOME/uvm_pkg.sv if not present in
|
|
510
|
+
self.files already (adds to front of self.files_sv)
|
|
511
|
+
'''
|
|
512
|
+
|
|
513
|
+
if not self.args['uvm']:
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
for fname, exists in self.files.items():
|
|
517
|
+
if exists and os.path.split(fname)[1] == 'uvm_pkg.sv':
|
|
518
|
+
# already present in our source files (assume someone doing it manually
|
|
519
|
+
# or via DEPS)
|
|
520
|
+
return True
|
|
521
|
+
|
|
522
|
+
uvm_home = os.environ.get('UVM_HOME', '')
|
|
523
|
+
if not uvm_home:
|
|
524
|
+
return False
|
|
525
|
+
|
|
526
|
+
uvm_pkg_fpath = os.path.join(uvm_home, 'uvm_pkg.sv')
|
|
527
|
+
if add_if_found and os.path.isfile(uvm_pkg_fpath):
|
|
528
|
+
uvm_pkg_fpath = os.path.abspath(uvm_pkg_fpath)
|
|
529
|
+
util.info(f'For --uvm, adding to source files: {uvm_pkg_fpath}')
|
|
530
|
+
self.files[uvm_pkg_fpath] = True
|
|
531
|
+
self.files_sv.insert(0, uvm_pkg_fpath)
|
|
532
|
+
self.files_caller_info[uvm_pkg_fpath] = 'verilator.py'
|
|
533
|
+
util.info(f'For --uvm, adding +incdir+: {uvm_home}')
|
|
534
|
+
self.incdirs.append(os.path.abspath(uvm_home))
|
|
535
|
+
return True
|
|
536
|
+
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _verilator_args_uvm(
|
|
541
|
+
self, warnings: bool = True, add_uvm_pkg_if_found: bool = True
|
|
542
|
+
) -> list:
|
|
543
|
+
'''Returns list of args to be added to verilator (compile) step if --uvm present
|
|
544
|
+
|
|
545
|
+
Warnings on potential issues (Veriltor version, missing uvm_pkg.sv).
|
|
546
|
+
Optionally adds uvm_pkg.sv to source files.
|
|
547
|
+
'''
|
|
548
|
+
|
|
549
|
+
# Handle --uvm args:
|
|
550
|
+
if not self.args['uvm']:
|
|
551
|
+
return []
|
|
552
|
+
|
|
553
|
+
if warnings:
|
|
554
|
+
|
|
555
|
+
# prefers Verilator >= v5.042, $UVM_HOME to be set, or warning.
|
|
556
|
+
version_list = self._VERSION.split('.')
|
|
557
|
+
if int(version_list[0]) < 5 or \
|
|
558
|
+
(int(version_list[0]) == 5 and int(version_list[1]) < 42):
|
|
559
|
+
util.warning(f'Verilator version is {self._VERSION}, --uvm set prefers Verilator',
|
|
560
|
+
'version > v5.042')
|
|
561
|
+
|
|
562
|
+
if not os.environ.get('UVM_HOME', ''):
|
|
563
|
+
util.warning('--uvm set, however env (or .env or --env-file) $UVM_HOME is not set')
|
|
564
|
+
|
|
565
|
+
uvm_pkg_found = self._verilator_support_uvm_pkg_fpath(add_if_found=add_uvm_pkg_if_found)
|
|
566
|
+
if warnings and not uvm_pkg_found:
|
|
567
|
+
util.warning(
|
|
568
|
+
'--uvm set, however no suitable uvm_pkg.sv is source files,',
|
|
569
|
+
f'nor in $UVM_HOME/uvm_pkg.sv. $UVM_HOME={os.environ.get("UVM_HOME", "")}'
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return ['-Wno-fatal', '+define+UVM_NO_DPI']
|
|
573
|
+
|
|
574
|
+
|
|
480
575
|
def artifacts_add(self, name: str, typ: str, description: str) -> None:
|
|
481
576
|
'''Override from Command.artifacts_add, so we can catch known file
|
|
482
577
|
|
opencos/tools/vivado.py
CHANGED
|
@@ -272,6 +272,7 @@ class CommandSimVivado(CommandSim, ToolVivado):
|
|
|
272
272
|
xsim_plusargs_list.append('--testplusarg')
|
|
273
273
|
if x[0] == '+':
|
|
274
274
|
x = x[1:]
|
|
275
|
+
x = x.replace('"', '\\\"') # we have to preserve " in the value.
|
|
275
276
|
xsim_plusargs_list.append(f'\"{x}\"')
|
|
276
277
|
|
|
277
278
|
# execute snapshot
|
opencos/util.py
CHANGED
|
@@ -18,8 +18,10 @@ from enum import Enum
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from importlib import import_module
|
|
20
20
|
from dotenv import load_dotenv
|
|
21
|
+
from supports_color import supportsColor
|
|
21
22
|
|
|
22
23
|
from opencos.utils import status_constants
|
|
24
|
+
from opencos.utils.str_helpers import strip_ansi_color
|
|
23
25
|
|
|
24
26
|
global_exit_allowed = False # pylint: disable=invalid-name
|
|
25
27
|
progname = "UNKNOWN" # pylint: disable=invalid-name
|
|
@@ -29,7 +31,7 @@ dot_f_files_expanded = set() # pylint: disable=invalid-name
|
|
|
29
31
|
env_files_loaded = set() # pylint: disable=invalid-name
|
|
30
32
|
|
|
31
33
|
args = { # pylint: disable=invalid-name
|
|
32
|
-
'color' :
|
|
34
|
+
'color' : bool(supportsColor.stdout),
|
|
33
35
|
'quiet' : False,
|
|
34
36
|
'verbose' : False,
|
|
35
37
|
'debug' : False,
|
|
@@ -351,13 +353,14 @@ def get_argparser() -> argparse.ArgumentParser:
|
|
|
351
353
|
bool_action_kwargs = get_argparse_bool_action_kwargs()
|
|
352
354
|
|
|
353
355
|
parser.add_argument('--version', default=False, action='store_true')
|
|
354
|
-
parser.add_argument('--color', **bool_action_kwargs, default=
|
|
356
|
+
parser.add_argument('--color', **bool_action_kwargs, default=bool(supportsColor.stdout),
|
|
355
357
|
help='Use shell colors for info/warning/error messaging')
|
|
356
|
-
parser.add_argument('--quiet', **bool_action_kwargs,
|
|
357
|
-
|
|
358
|
+
parser.add_argument('--quiet', **bool_action_kwargs, default=args['quiet'],
|
|
359
|
+
help='Do not display info messaging')
|
|
360
|
+
parser.add_argument('--verbose', **bool_action_kwargs, default=args['verbose'],
|
|
358
361
|
help='Display additional messaging level 2 or higher')
|
|
359
|
-
parser.add_argument('--fancy', **bool_action_kwargs)
|
|
360
|
-
parser.add_argument('--debug', **bool_action_kwargs,
|
|
362
|
+
parser.add_argument('--fancy', **bool_action_kwargs, default=args['fancy'])
|
|
363
|
+
parser.add_argument('--debug', **bool_action_kwargs, default=args['debug'],
|
|
361
364
|
help='Display additional debug messaging level 1 or higher')
|
|
362
365
|
parser.add_argument('--debug-level', type=int, default=0,
|
|
363
366
|
help='Set debug level messaging (default: 0)')
|
|
@@ -381,8 +384,10 @@ def get_argparser() -> argparse.ArgumentParser:
|
|
|
381
384
|
'Input .f file to be expanded as eda'
|
|
382
385
|
' args/defines/incdirs/files/targets'))
|
|
383
386
|
parser.add_argument('--env-file', default=[], action='append',
|
|
384
|
-
help=
|
|
385
|
-
|
|
387
|
+
help=(
|
|
388
|
+
"dotenv file(s) to pass ENV vars, (default: .env loaded first,"
|
|
389
|
+
" subsequent files' vars override .env"
|
|
390
|
+
))
|
|
386
391
|
return parser
|
|
387
392
|
|
|
388
393
|
|
|
@@ -405,10 +410,10 @@ def get_argparser_short_help(parser: object = None) -> str:
|
|
|
405
410
|
'''Returns short help for our ArgumentParser'''
|
|
406
411
|
if not parser:
|
|
407
412
|
parser = get_argparser()
|
|
408
|
-
full_lines = parser.format_help().split('\n')
|
|
413
|
+
full_lines = strip_ansi_color(parser.format_help()).split('\n')
|
|
409
414
|
lineno = 0
|
|
410
415
|
for lineno, line in enumerate(full_lines):
|
|
411
|
-
if line.startswith('options:'):
|
|
416
|
+
if any(line.startswith(x) for x in ('options:', 'optional arguments:')):
|
|
412
417
|
break
|
|
413
418
|
# skip the line that says 'options:', repalce with the progname:
|
|
414
419
|
return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
|
|
@@ -430,11 +435,12 @@ def process_token(arg: list) -> bool:
|
|
|
430
435
|
def load_env_file(env_file: str) -> None:
|
|
431
436
|
'''Handles .env file (from util CLI args --env-file)'''
|
|
432
437
|
if os.path.isfile(env_file):
|
|
433
|
-
load_dotenv(env_file)
|
|
438
|
+
load_dotenv(env_file, override=True)
|
|
434
439
|
env_files_loaded.add(os.path.abspath(env_file))
|
|
435
440
|
else:
|
|
436
441
|
warning(f'--env-file {env_file} does not exist and is not loaded.')
|
|
437
442
|
|
|
443
|
+
|
|
438
444
|
def patch_args_for_dir(tokens: list, patch_dir: str, caller_info: str) -> list:
|
|
439
445
|
'''Given list of args, attempt to correct for relative dir'''
|
|
440
446
|
|
|
@@ -534,8 +540,10 @@ def process_tokens( # pylint: disable=too-many-branches
|
|
|
534
540
|
parser.add_argument('--debug-level', type=int, default=0,
|
|
535
541
|
help='Set debug level messaging (default: 0)')
|
|
536
542
|
parser.add_argument('--env-file', default=[], action='append',
|
|
537
|
-
help=
|
|
538
|
-
|
|
543
|
+
help=(
|
|
544
|
+
"dotenv file(s) to pass ENV vars, (default: .env loaded first,"
|
|
545
|
+
" subsequent files' vars override .env"
|
|
546
|
+
))
|
|
539
547
|
parser.add_argument('-f', '--input-file', default=[], action='append',
|
|
540
548
|
help=(
|
|
541
549
|
'Input .f file to be expanded as eda args, defines, incdirs,'
|
|
@@ -551,7 +559,7 @@ def process_tokens( # pylint: disable=too-many-branches
|
|
|
551
559
|
debug(f'util.process_tokens: {parsed=} {unparsed=} from: {tokens}')
|
|
552
560
|
|
|
553
561
|
if os.path.isfile(str(Path('.env'))):
|
|
554
|
-
parsed.env_file.
|
|
562
|
+
parsed.env_file.insert(0, '.env')
|
|
555
563
|
if parsed.env_file:
|
|
556
564
|
for env_file in parsed.env_file:
|
|
557
565
|
load_env_file(env_file)
|
opencos/utils/str_helpers.py
CHANGED
|
@@ -172,7 +172,7 @@ def pretty_list_columns_manual(data: list, num_columns: int = 4, auto_columns: b
|
|
|
172
172
|
pretty_list_columns_manual(data=data, num_columns=num_columns-1, auto_columns=True)
|
|
173
173
|
)
|
|
174
174
|
return ret_lines
|
|
175
|
-
if max_line_len + max_item_len + _spacing
|
|
175
|
+
if max_line_len + max_item_len + _spacing <= window_cols:
|
|
176
176
|
# add 1 more column if we're guaranteed to have room.
|
|
177
177
|
ret_lines.extend(
|
|
178
178
|
pretty_list_columns_manual(data=data, num_columns=num_columns+1, auto_columns=True)
|
|
@@ -198,3 +198,8 @@ def print_columns_manual(data: list, num_columns: int = 4, auto_columns: bool =
|
|
|
198
198
|
data=data, num_columns=num_columns, auto_columns=auto_columns
|
|
199
199
|
)
|
|
200
200
|
print('\n'.join(lines))
|
|
201
|
+
|
|
202
|
+
def strip_ansi_color(text: str) -> str:
|
|
203
|
+
'''Strip ANSI color characters from str'''
|
|
204
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
205
|
+
return ansi_escape.sub('', text)
|
|
@@ -5,10 +5,18 @@ import shutil
|
|
|
5
5
|
import subprocess
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import psutil
|
|
9
|
+
from opencos.util import debug, error, info, warning, progname, global_log
|
|
9
10
|
|
|
10
11
|
IS_WINDOWS = sys.platform.startswith('win')
|
|
11
12
|
|
|
13
|
+
# For non-Windows, we track the background parent PIDs, because some tools (vivado XSim,
|
|
14
|
+
# most Modelsim/Questa variants) tend to spawn children PIDs that don't always respond
|
|
15
|
+
# nicely to a friendly *nix SIGTERM. So we'll remember what our parent PIDs are, and
|
|
16
|
+
# eda.py's (or other CLI opencos script) can use signal to cleanup any remaining
|
|
17
|
+
# parents + children using subprocess_helpers.cleanup_all()
|
|
18
|
+
ALL_PARENT_PIDS = set()
|
|
19
|
+
|
|
12
20
|
def subprocess_run(
|
|
13
21
|
work_dir: str, command_list: list, fake: bool = False, shell: bool = False
|
|
14
22
|
) -> int:
|
|
@@ -35,10 +43,11 @@ def subprocess_run(
|
|
|
35
43
|
|
|
36
44
|
debug(f"subprocess_run: About to call subprocess.run({c}, **{proc_kwargs}")
|
|
37
45
|
proc = subprocess.run(c, check=True, **proc_kwargs)
|
|
46
|
+
# Note - we do not get PID management for subprocess_run(...)
|
|
38
47
|
return proc.returncode
|
|
39
48
|
|
|
40
49
|
|
|
41
|
-
def subprocess_run_background(
|
|
50
|
+
def subprocess_run_background( # pylint: disable=too-many-branches
|
|
42
51
|
work_dir: str, command_list: list, background: bool = True, fake : bool = False,
|
|
43
52
|
shell: bool = False, tee_fpath: str = ''
|
|
44
53
|
) -> (str, str, int):
|
|
@@ -76,6 +85,9 @@ def subprocess_run_background(
|
|
|
76
85
|
|
|
77
86
|
debug(f"subprocess_run_background: about to call subprocess.Popen({c}, **{proc_kwargs})")
|
|
78
87
|
proc = subprocess.Popen(c, **proc_kwargs) # pylint: disable=consider-using-with
|
|
88
|
+
if not background:
|
|
89
|
+
info(f'PID {proc.pid} for {command_list[0]}')
|
|
90
|
+
add_running_parent_pid(proc.pid)
|
|
79
91
|
|
|
80
92
|
stdout = ''
|
|
81
93
|
tee_fpath_f = None
|
|
@@ -99,10 +111,61 @@ def subprocess_run_background(
|
|
|
99
111
|
stdout += line + '\n'
|
|
100
112
|
|
|
101
113
|
proc.communicate()
|
|
114
|
+
remove_completed_parent_pid(proc.pid)
|
|
115
|
+
|
|
102
116
|
rc = proc.returncode
|
|
103
117
|
if tee_fpath_f:
|
|
104
118
|
tee_fpath_f.write(f'INFO: [{progname}] subprocess_run_background: returncode={rc}\n')
|
|
105
119
|
tee_fpath_f.close()
|
|
106
|
-
|
|
120
|
+
if not background:
|
|
121
|
+
info('subprocess_run_background: wrote: ' + os.path.abspath(tee_fpath))
|
|
107
122
|
|
|
108
123
|
return stdout, '', rc
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def add_running_parent_pid(pid: int) -> None:
|
|
127
|
+
'''Adds pid (if still alive) to ALL_PARENT_PIDS'''
|
|
128
|
+
try:
|
|
129
|
+
p = psutil.Process(pid)
|
|
130
|
+
ALL_PARENT_PIDS.add(p.pid)
|
|
131
|
+
except psutil.NoSuchProcess:
|
|
132
|
+
pass
|
|
133
|
+
except Exception as e:
|
|
134
|
+
error(f'{pid=} exception {e}')
|
|
135
|
+
|
|
136
|
+
def remove_completed_parent_pid(pid: int) -> None:
|
|
137
|
+
'''Removes pid (if no longer alive) from ALL_PARENT_PIDS.'''
|
|
138
|
+
try:
|
|
139
|
+
p = psutil.Process(pid)
|
|
140
|
+
warning(f'PID {p.pid} still running')
|
|
141
|
+
except psutil.NoSuchProcess:
|
|
142
|
+
ALL_PARENT_PIDS.remove(pid)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
error(f'{pid=} exception {e}')
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cleanup_all() -> None:
|
|
148
|
+
'''Kills everything from ALL_PARENT_PIDS.'''
|
|
149
|
+
for parent in ALL_PARENT_PIDS:
|
|
150
|
+
kill_proc_tree(parent)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def kill_proc_tree(pid: int, including_parent: bool = True) -> None:
|
|
154
|
+
'''Kills a process and its entire descendant tree'''
|
|
155
|
+
try:
|
|
156
|
+
parent = psutil.Process(pid)
|
|
157
|
+
children = parent.children(recursive=True)
|
|
158
|
+
info(f'{pid=} {parent=} {children=}')
|
|
159
|
+
for child in children:
|
|
160
|
+
if psutil.Process(child.pid):
|
|
161
|
+
info(f'parent {pid=} killing {child=}')
|
|
162
|
+
child.kill()
|
|
163
|
+
_, still_alive = psutil.wait_procs(children, timeout=5)
|
|
164
|
+
if still_alive:
|
|
165
|
+
warning(f'parent {pid=} {still_alive=}')
|
|
166
|
+
if including_parent:
|
|
167
|
+
info(f'parent {pid=} killing {parent=}')
|
|
168
|
+
parent.kill()
|
|
169
|
+
parent.wait(5)
|
|
170
|
+
except psutil.NoSuchProcess:
|
|
171
|
+
pass # Process already terminated
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencos-eda
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: A simple Python package for wrapping RTL simuliatons and synthesis
|
|
5
5
|
Author-email: Simon Sabato <simon@cognichip.ai>, Drew Ranck <drew@cognichip.ai>
|
|
6
6
|
Project-URL: Homepage, https://github.com/cognichip/opencos
|
|
@@ -9,12 +9,21 @@ License-File: LICENSE
|
|
|
9
9
|
License-File: LICENSE.spdx
|
|
10
10
|
Requires-Dist: mergedeep>=1.3.4
|
|
11
11
|
Requires-Dist: peakrdl>=1.1.0
|
|
12
|
+
Requires-Dist: psutil>=7.0.0
|
|
12
13
|
Requires-Dist: pyyaml>=6.0.2
|
|
13
|
-
Requires-Dist: pytest>=8.3.5
|
|
14
14
|
Requires-Dist: python-dotenv>=1.0.1
|
|
15
15
|
Requires-Dist: schema>=0.7.7
|
|
16
16
|
Requires-Dist: toml>=0.10.2
|
|
17
17
|
Requires-Dist: yamllint>=1.35.1
|
|
18
18
|
Requires-Dist: PySerial>=3.5
|
|
19
19
|
Requires-Dist: cocotb>=2.0
|
|
20
|
+
Requires-Dist: supports_color>=0.2.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pylint>=3.0.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
24
|
+
Provides-Extra: docs
|
|
25
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
27
|
+
Requires-Dist: mkdocs-wavedrom-plugin; extra == "docs"
|
|
28
|
+
Requires-Dist: mkdocs-plantuml; extra == "docs"
|
|
20
29
|
Dynamic: license-file
|