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/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(self, dot_do_to_write: list) -> None:
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
- vsim_dot_do_lines += [
283
- "run -all;",
284
- "acdb save",
285
- "acdb report -db work.acdb -txt -o cov.txt",
286
- # Note - could try:
287
- ##"cover report -o cov.report.txt -fullverbose -all_columns",
288
- ]
289
- else:
290
- vsim_dot_do_lines += [
291
- "run -all;",
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.'''
@@ -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
- # Add compile args from our self.config (tools.verilator.compile-args str)
214
- config_compile_args = self.tool_config.get(
215
- 'compile-args',
216
- '--timing --assert --autoflush -sv').split()
217
- verilate_command_list += config_compile_args
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._verilator_args_deal_with_cflags()
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 _verilator_args_deal_with_cflags(self) -> list:
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'] and self.args['compile-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 self.args['verilate-args']:
435
- verilate_cflags_args_dict = {}
436
- verilate_args = [] # will be combined verilate_args + compile-args
437
- prev_arg_is_cflags = False
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
- if '-CFLAGS' in verilate_args:
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' : False,
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=True,
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, help='Do not display info messaging')
357
- parser.add_argument('--verbose', **bool_action_kwargs,
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='dotenv file(s) to pass ENV vars, (default: .env, loaded last)'
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='dotenv file(s) to pass ENV vars, (default: .env, loaded last)'
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.append('.env')
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)
@@ -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 < window_cols:
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
- from opencos.util import debug, error, info, progname, global_log
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
- info('subprocess_run_background: wrote: ' + os.path.abspath(tee_fpath))
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.2
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