opencos-eda 0.3.6__py3-none-any.whl → 0.3.8__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.
@@ -3,16 +3,16 @@
3
3
  # pylint: disable=R0801 # (similar lines in 2+ files)
4
4
 
5
5
  import os
6
+ import shutil
6
7
  import sys
7
8
  import pytest
8
9
 
9
- from opencos import eda, eda_base
10
-
10
+ from opencos import eda_base
11
11
  from opencos.tools.verilator import ToolVerilator
12
12
  from opencos.tools.vivado import ToolVivado
13
13
  from opencos.tools.cocotb import ToolCocotb
14
14
  from opencos.tests import helpers
15
- from opencos.tests.helpers import eda_wrap, eda_wrap_is_sim_fail, config, tools_loaded
15
+ from opencos.tests.helpers import Helpers, eda_wrap, eda_wrap_is_sim_fail, config, tools_loaded
16
16
  from opencos.utils.markup_helpers import yaml_safe_load
17
17
  from opencos.utils import status_constants
18
18
 
@@ -23,6 +23,10 @@ def chdir_remove_work_dir(relpath):
23
23
  '''Changes dir to relpath, removes the work directories (eda.work, eda.export*)'''
24
24
  return helpers.chdir_remove_work_dir(THISPATH, relpath)
25
25
 
26
+ def filter_tools(tools: list) -> list:
27
+ '''Given a list of tool l, filters to return a list of tools that are loaded'''
28
+ return [x for x in tools if x in tools_loaded]
29
+
26
30
 
27
31
  def test_tools_loaded():
28
32
  '''Does not directly call 'eda.main' instead create a few Tool
@@ -32,14 +36,19 @@ def test_tools_loaded():
32
36
  assert config
33
37
  assert len(config.keys()) > 0
34
38
 
39
+
35
40
  # It's possible we're running in some container or install that has no tools, for example,
36
41
  # Windows.
37
42
  if sys.platform.startswith('win') and \
38
43
  not helpers.can_run_eda_command('elab', 'sim', cfg=config):
39
- # Windows, not handlers for elab or sim:
44
+ # Windows, no handlers for elab or sim:
40
45
  pass
41
46
  else:
42
- assert len(tools_loaded) > 0
47
+ basic_tools_available_via_path = any(shutil.which(x) for x in (
48
+ 'verilator', 'iverilog', 'xsim', 'vsim', 'slang', 'yosys'
49
+ ))
50
+ if basic_tools_available_via_path:
51
+ assert len(tools_loaded) > 0
43
52
 
44
53
  def version_checker(
45
54
  obj: eda_base.Tool, chk_str: str
@@ -49,7 +58,7 @@ def test_tools_loaded():
49
58
  assert chk_str in full_ver, f'{chk_str=} not in {full_ver=}'
50
59
  ver_num = full_ver.rsplit(':', maxsplit=1)[-1]
51
60
  if 'b' in ver_num:
52
- ver_num = ver_num.split('b')[0] # TODO(chaitanya): remove once cocotb 2.0 is released
61
+ ver_num = ver_num.split('b')[0]
53
62
  if '.' in ver_num:
54
63
  major_ver = ver_num.split('.')[0]
55
64
  assert major_ver.isdigit(), (
@@ -101,40 +110,60 @@ list_of_added_sim_args = [
101
110
  '--gui --test-mode',
102
111
  ]
103
112
 
113
+ list_of_loaded_tools = filter_tools(list_of_tools)
114
+
104
115
  cannot_use_cocotb = 'cocotb' not in tools_loaded or \
105
116
  ('iverilog' not in tools_loaded and \
106
117
  'verilator' not in tools_loaded)
107
118
  CANNOT_USE_COCOTB_REASON = 'requires cocotb in tools_loaded, and one of (iverilog, verilator) too'
108
119
 
109
- @pytest.mark.parametrize("command", list_of_commands)
110
- @pytest.mark.parametrize("tool", list_of_tools)
111
- @pytest.mark.parametrize("target,sim_expect_pass", list_of_deps_targets)
112
- @pytest.mark.parametrize("added_sim_args_str", list_of_added_sim_args)
113
- def test_sim_elab_tools_pass_or_fail(command, tool, target, sim_expect_pass, added_sim_args_str):
114
- '''tests that: eda <sim|elab> --tool <parameter-tool> <parameter-args> <parameter-target>
115
120
 
116
- will correctly pass or fail depending on if it is supported or not.
121
+ class TestSimElabTools(Helpers):
122
+ '''Tests for eda sim|elab for various tools with various args'''
117
123
 
118
- Also tests for: non-gui, or --gui --test-mode (runs non-gui, but most python args will
119
- be for --gui mode, signal logging, etc).
120
- '''
121
- if tool not in tools_loaded:
122
- pytest.skip(f"{tool=} skipped, {tools_loaded=}")
123
- return # skip/pass
124
-
125
- added_args = []
126
- if command == 'sim':
127
- added_args = added_sim_args_str.split()
128
-
129
- relative_dir = "deps_files/test_err_fatal"
130
- os.chdir(os.path.join(THISPATH, relative_dir))
131
- rc = eda.main(command, '--tool', tool, *(added_args), target)
132
- print(f'{rc=}')
133
- if command != 'sim' or sim_expect_pass:
134
- # command='elab' should pass.
135
- assert rc == 0
136
- else:
137
- assert eda_wrap_is_sim_fail(rc)
124
+ DEFAULT_DIR = os.path.join(THISPATH, 'deps_files', 'test_err_fatal')
125
+
126
+ @pytest.mark.parametrize("command", list_of_commands)
127
+ @pytest.mark.parametrize("tool", list_of_loaded_tools)
128
+ @pytest.mark.parametrize("target,sim_expect_pass", list_of_deps_targets)
129
+ @pytest.mark.parametrize("added_sim_args_str", list_of_added_sim_args)
130
+ def test_pass_or_fail(
131
+ self, command, tool, target, sim_expect_pass, added_sim_args_str
132
+ ):
133
+ '''tests that: eda <sim|elab> --tool <parameter-tool> <parameter-args> <parameter-target>
134
+
135
+ will correctly pass or fail depending on if it is supported or not.
136
+
137
+ Also tests for: non-gui, or --gui --test-mode (runs non-gui, but most python args will
138
+ be for --gui mode, signal logging, etc).
139
+ '''
140
+ added_args_str = ''
141
+ if command == 'sim':
142
+ added_args_str = added_sim_args_str
143
+
144
+ rc = self.log_it(f'{command} --tool {tool} {added_args_str} {target}')
145
+ print(f'{rc=}')
146
+ tool_error_count_lines = self.get_log_lines_with('tool errors')
147
+ if command != 'sim' or sim_expect_pass:
148
+ # command='elab' should pass.
149
+ assert rc == 0
150
+ assert tool_error_count_lines
151
+ assert all('tool warnings' in line for line in tool_error_count_lines)
152
+ assert all(' 0 tool errors' in line for line in tool_error_count_lines)
153
+
154
+ else:
155
+ assert eda_wrap_is_sim_fail(rc)
156
+ assert tool_error_count_lines
157
+ assert all('tool warnings' in line for line in tool_error_count_lines)
158
+ # May or may not have reported tool errors.
159
+ assert all('tool errors' in line for line in tool_error_count_lines)
160
+ # The final line of tool warnings/errors should have > 0 tool errors,
161
+ # since we were checking SV $error and $fatal
162
+ assert ' 0 tool errors' not in tool_error_count_lines[-1]
163
+ # The line should end with ' X tool errors'
164
+ parts = tool_error_count_lines[-1].split()
165
+ assert parts[-3].isdigit()
166
+ assert int(parts[-3]) in (1, 2, 3) # we should have at least 1, but some tools dup.
138
167
 
139
168
 
140
169
  @pytest.mark.skipif('vivado' not in tools_loaded, reason="requires vivado")
opencos/tools/iverilog.py CHANGED
@@ -93,7 +93,7 @@ class CommandSimIverilog(CommandSim, ToolIverilog):
93
93
  def compile(self):
94
94
  if self.args['stop-before-compile']:
95
95
  return
96
- self.run_commands_check_logs(self.iverilog_command_lists, check_logs=False)
96
+ self.run_commands_check_logs(self.iverilog_command_lists)
97
97
 
98
98
  def elaborate(self):
99
99
  pass
@@ -148,7 +148,7 @@ class CommandSimIverilog(CommandSim, ToolIverilog):
148
148
 
149
149
  command_list += list(self.files_sv) + list(self.files_v)
150
150
 
151
- return [ util.ShellCommandList(command_list) ]
151
+ return [ util.ShellCommandList(command_list, tee_fpath='compile.log') ]
152
152
 
153
153
  def get_elaborate_command_lists(self, **kwargs) -> list:
154
154
  return []
opencos/tools/riviera.py CHANGED
@@ -21,6 +21,7 @@ class ToolRiviera(ToolModelsimAse):
21
21
  _TOOL = 'riviera'
22
22
  _EXE = 'vsim'
23
23
  use_vopt = False
24
+ uvm_versions = set()
24
25
 
25
26
  def get_versions(self) -> str:
26
27
  if self._VERSION:
@@ -40,7 +41,17 @@ class ToolRiviera(ToolModelsimAse):
40
41
  )
41
42
  stdout = version_ret.stdout.decode('utf-8', errors='replace').rstrip()
42
43
 
43
- # Expect:
44
+ # Get the UVM versions in the install directory. Note this may run
45
+ # more than once, so only do this if self.uvm_versions not yet set:
46
+ riviera_path, _ = os.path.split(self.sim_exe_base_path)
47
+ vlib_path = os.path.join(riviera_path, 'vlib')
48
+ if not self.uvm_versions and os.path.isdir(vlib_path):
49
+ for item in os.listdir(vlib_path):
50
+ # uvm-1.1, uvm-1.1d - so don't pick anything > 9 chars (uvm-M.mRr)
51
+ if item.startswith('uvm-') and '1800' not in item and len(item) <= 9:
52
+ self.uvm_versions.add(item[4:])
53
+
54
+ # For Version, expect:
44
55
  # Aldec, Inc. Riviera-PRO version 2025.04.139.9738 built for Linux64 on May 30, 2025
45
56
  left, right = stdout.split('version')
46
57
  if 'Riviera' not in left:
@@ -76,6 +87,18 @@ class CommandSimRiviera(CommandSimModelsimAse, ToolRiviera):
76
87
  'bring your own .tcl file to run in Riviera (vsim) for coverage. The default'
77
88
  ' tcl steps are (from tool config in --config-yml): '
78
89
  ) + '; '.join(self.tool_config.get('simulate-coverage-tcl', [])),
90
+ 'uvm': (
91
+ 'Attempts to support UVM. Adds to vlog: -l uvm +incdir+PATH for the PATH to'
92
+ ' uvm_macros.svh for the installed version of Riviera used.'
93
+ ),
94
+ })
95
+
96
+ if self.uvm_versions:
97
+ # set default to latest version:
98
+ self.args['uvm-version'] = sorted(self.uvm_versions)[-1]
99
+
100
+ self.args_kwargs.update({
101
+ 'uvm-version': { 'choices': list(self.uvm_versions) }
79
102
  })
80
103
 
81
104
  def set_tool_defines(self):
@@ -136,7 +159,9 @@ class CommandSimRiviera(CommandSimModelsimAse, ToolRiviera):
136
159
  return []
137
160
 
138
161
 
139
- def write_vlog_dot_f(self, filename='vlog.f') -> None:
162
+ def write_vlog_dot_f( # pylint: disable=too-many-branches
163
+ self, filename: str = 'vlog.f'
164
+ ) -> None:
140
165
  '''Returns none, creates filename (str) for a vlog.f'''
141
166
  vlog_dot_f_lines = []
142
167
 
@@ -153,6 +178,12 @@ class CommandSimRiviera(CommandSimModelsimAse, ToolRiviera):
153
178
  vlog_dot_f_fname = filename
154
179
  vlog_dot_f_fpath = os.path.join(self.args['work-dir'], vlog_dot_f_fname)
155
180
 
181
+ if self.args['uvm']:
182
+ vlog_dot_f_lines.extend([
183
+ f'-uvmver {self.args["uvm-version"]}',
184
+ '-dbg'
185
+ ])
186
+
156
187
  for value in self.incdirs:
157
188
  vlog_dot_f_lines += [ f"+incdir+{value}" ]
158
189
 
opencos/tools/slang.py CHANGED
@@ -176,6 +176,30 @@ class CommandElabSlang(CommandElab, ToolSlang):
176
176
  def get_post_simulate_command_lists(self, **kwargs) -> list:
177
177
  return []
178
178
 
179
+ def update_tool_warn_err_counts_from_log_lines(
180
+ self, log_lines: list, bad_strings: list, warning_strings: list
181
+ ) -> None:
182
+ '''
183
+ Overriden from Command, we ignore bad_strings/warning_strings and use a custom
184
+ checker.
185
+ '''
186
+ for line in log_lines:
187
+ if not line.startswith('Build failed: '):
188
+ continue
189
+ if not all(x in line for x in ('errors', 'warnings')):
190
+ continue
191
+
192
+ parts = line.strip().split()
193
+ if len(parts) < 6:
194
+ continue
195
+
196
+ errs = parts[2]
197
+ warns = parts[4]
198
+ if errs.isdigit():
199
+ self.tool_error_count += int(errs)
200
+ if warns.isdigit():
201
+ self.tool_warning_count += int(warns)
202
+
179
203
  def _get_slang_command_list_start(self) -> list:
180
204
  command_list = [self.slang_exe]
181
205
 
opencos/tools/surelog.py CHANGED
@@ -147,6 +147,28 @@ class CommandElabSurelog(CommandElab, ToolSurelog):
147
147
  command_lists=self.surelog_command_lists, line_breaks=True
148
148
  )
149
149
 
150
+ def update_tool_warn_err_counts_from_log_lines(
151
+ self, log_lines: list, bad_strings: list, warning_strings: list
152
+ ) -> None:
153
+ '''
154
+ Overriden from Command, we ignore bad_strings/warning_strings and use a custom
155
+ checker.
156
+ '''
157
+ for line in log_lines:
158
+ line = line.strip()
159
+ if line.endswith(' 0'):
160
+ continue
161
+ if line.startswith('[ FATAL] : ') or \
162
+ line.startswith('[ SYNTAX] : ') or \
163
+ line.startswith('[ ERROR] : '):
164
+ parts = line.split()
165
+ if parts[-1].isdigit():
166
+ self.tool_error_count += int(parts[-1])
167
+ if line.startswith('[WARNING] : '):
168
+ parts = line.split()
169
+ if parts[-1].isdigit():
170
+ self.tool_warning_count += int(parts[-1])
171
+
150
172
 
151
173
  class CommandLintSurelog(CommandElabSurelog):
152
174
  '''CommandLintSurelog is a command handler for: eda lint --tool=surelog.'''
@@ -76,14 +76,12 @@ class VerilatorSim(CommandSim, ToolVerilator):
76
76
  ToolVerilator.__init__(self, config=self.config)
77
77
  self.args.update({
78
78
  'gui': False,
79
- 'tcl-file': None,
80
79
  'dump-vcd': False,
81
80
  'waves-fst': True,
82
81
  'waves-vcd': False,
83
82
  'lint-only': False,
84
83
  'cc-mode': False,
85
84
  'verilator-coverage-args': [],
86
- 'uvm': False,
87
85
  'x-assign': '',
88
86
  'x-initial': '',
89
87
  })
@@ -95,7 +93,7 @@ class VerilatorSim(CommandSim, ToolVerilator):
95
93
  ' plusarg and apply $dumpfile("dump.fst").'
96
94
  ),
97
95
  'waves-fst': (
98
- '(Default True) If using --waves, apply simulation runtime arg +trace.'
96
+ 'If using --waves, apply simulation runtime arg +trace.'
99
97
  ' Note that if you do not have SV code using $dumpfile, eda will add'
100
98
  ' _waves_pkg.sv to handle this for you with +trace runtime plusarg.'
101
99
  ),
@@ -118,9 +116,9 @@ class VerilatorSim(CommandSim, ToolVerilator):
118
116
  ' Also conditinally adds to verilated exe call:'
119
117
  ' +verilator+rand+reset+[0,2] for arg values 0, unique|fast'),
120
118
  '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'
119
+ 'Enables UVM. Warns on Verilator < 5.042, or missing $UVM_HOME environment'
120
+ ' var set (or in .env, $UVM_HOME/uvm_pkg.sv should exist), and will run verilator'
121
+ ' with args: -Wno-fatal +define+UVM_NO_DPI'
124
122
  ),
125
123
  'verilator-coverage-args': (
126
124
  'Requires --coverage, args to be applied to verilator_coverage, which runs'
@@ -128,6 +126,12 @@ class VerilatorSim(CommandSim, ToolVerilator):
128
126
  ),
129
127
  })
130
128
 
129
+
130
+ self.args_kwargs.update({
131
+ 'x-assign': { 'choices': ['0', '1', 'unique', 'fast'] },
132
+ 'x-initial': { 'choices': ['0', 'unique', 'fast'] },
133
+ })
134
+
131
135
  self.verilate_command_lists = []
132
136
  self.lint_only_command_lists = []
133
137
  self.verilated_exec_command_lists = []
@@ -563,7 +567,8 @@ class VerilatorSim(CommandSim, ToolVerilator):
563
567
  'version > v5.042')
564
568
 
565
569
  if not os.environ.get('UVM_HOME', ''):
566
- util.warning('--uvm set, however env (or .env or --env-file) $UVM_HOME is not set')
570
+ util.warning('--uvm set, however env (or .env or --env-file)',
571
+ '$UVM_HOME is not set')
567
572
 
568
573
  uvm_pkg_found = self._verilator_support_uvm_pkg_fpath(add_if_found=add_uvm_pkg_if_found)
569
574
  if warnings and not uvm_pkg_found:
opencos/tools/vivado.py CHANGED
@@ -225,6 +225,8 @@ class CommandSimVivado(CommandSim, ToolVivado):
225
225
 
226
226
  if self.tool_config.get('elab-waves-args', ''):
227
227
  command_list += self.tool_config.get('elab-waves-args', '').split()
228
+ if self.args['uvm']:
229
+ command_list.extend(['-L', 'uvm'])
228
230
  elif self.args['gui'] and self.args['waves']:
229
231
  command_list += ['-debug', 'all']
230
232
  elif self.args['gui']:
@@ -305,6 +307,8 @@ class CommandSimVivado(CommandSim, ToolVivado):
305
307
  command_list[0] += ".bat"
306
308
  if typ == 'sv':
307
309
  command_list.append('-sv')
310
+ if self.args['uvm']:
311
+ command_list.extend(['-L', 'uvm'])
308
312
  command_list += self.tool_config.get('compile-args', '').split()
309
313
  if util.args['verbose']:
310
314
  command_list += ['-v', '2']
opencos/tools/yosys.py CHANGED
@@ -70,9 +70,9 @@ class ToolYosys(Tool):
70
70
  [self.sta_exe, '-version'], capture_output=True, check=False
71
71
  )
72
72
  util.debug(f'{self.yosys_exe} {sta_version_ret=}')
73
- sta_ver = sta_version_ret.stdout.decode('utf-8', errors='replace').split()[0]
74
- if sta_ver:
75
- self.sta_version = sta_ver
73
+ sta_ver = sta_version_ret.stdout.decode('utf-8', errors='replace').split()
74
+ if sta_ver and isinstance(sta_ver, list):
75
+ self.sta_version = sta_ver[0]
76
76
 
77
77
  version_ret = subprocess.run(
78
78
  [self.yosys_exe, '--version'], capture_output=True, check=False
opencos/util.py CHANGED
@@ -54,7 +54,11 @@ class Colors:
54
54
  red = "\x1B[31m"
55
55
  green = "\x1B[32m"
56
56
  yellow = "\x1B[33m" # This looks orange, but it's techincally yellow
57
+ cyan = "\x1b[36m"
57
58
  foreground = "\x1B[39m"
59
+ bold = "\x1B[1m"
60
+ byellow = "\x1B[1m\x1B[33m"
61
+ bcyan = "\x1B[1m\x1b[36m"
58
62
  normal = "\x1B[0m"
59
63
 
60
64
  @staticmethod
@@ -67,6 +71,14 @@ class Colors:
67
71
  return color + text + "\x1B[0m" # (normal)
68
72
  return text
69
73
 
74
+ def disable(self) -> None:
75
+ '''Clears all color str in class Colors, to prevent print() methods that use
76
+ util.Colors from printing color'''
77
+ for x in dir(self):
78
+ if not callable(getattr(self, x, None)) and isinstance(x, str) and \
79
+ not x.startswith('_'):
80
+ setattr(self, x, '')
81
+
70
82
 
71
83
  def safe_emoji(emoji: str, default: str = '') -> str:
72
84
  '''Returns emoji character if args['emoji'] is True'''
@@ -421,7 +433,7 @@ def get_argparser_short_help(parser: object = None) -> str:
421
433
  break
422
434
 
423
435
  # skip the line that says 'options:', replace with the progname:
424
- return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
436
+ return f'{Colors.cyan}{parser.prog}:{Colors.normal}\n' + '\n'.join(full_lines[lineno + 1:])
425
437
 
426
438
 
427
439
  def process_token(arg: list) -> bool:
@@ -533,13 +545,14 @@ def process_tokens( # pylint: disable=too-many-branches
533
545
  global debug_level # pylint: disable=global-statement
534
546
  debug_level = 0
535
547
 
536
- # Deal with --debug, --debug-level, --env-file, -f/--input-file(s) tokens first,
537
- # for now put dot-f file contents in front of of tokens, do this first in a
538
- # separate custom argparser:
548
+ # Deal with --version, --debug, --debug-level, --env-file, -f/--input-file(s)
549
+ # tokens first, for now put dot-f file contents in front of of tokens, do this first
550
+ # in a separate custom argparser:
539
551
  bool_action_kwargs = get_argparse_bool_action_kwargs()
540
552
  parser = argparse.ArgumentParser(
541
553
  prog='opencos -f/--input-file', add_help=False, allow_abbrev=False
542
554
  )
555
+ parser.add_argument('--version', default=False, action='store_true')
543
556
  parser.add_argument('--debug', **bool_action_kwargs,
544
557
  help='Display additional debug messaging level 1 or higher')
545
558
  parser.add_argument('--debug-level', type=int, default=0,
@@ -560,6 +573,10 @@ def process_tokens( # pylint: disable=too-many-branches
560
573
  except argparse.ArgumentError:
561
574
  error(f'util -f/--input-file, problem attempting to parse_known_args for: {tokens}')
562
575
 
576
+ if parsed.version:
577
+ # Stop processing, return nothing, caller can print version and exit.
578
+ return parsed, []
579
+
563
580
  process_debug_args(parsed=parsed)
564
581
  debug(f'util.process_tokens: {parsed=} {unparsed=} from: {tokens}')
565
582
 
@@ -599,6 +616,9 @@ def process_tokens( # pylint: disable=too-many-branches
599
616
  warning(f'python error, nested -f/--input-file(s) {parsed.input_file} should',
600
617
  'have already been resolved')
601
618
 
619
+ if not parsed.color:
620
+ Colors.disable(Colors) # strip strings in Colors class
621
+
602
622
  # clear existing artifacts dicts (mostly for pytests repeatedly calling eda.main),
603
623
  # set artifacts.enabled based on args['artifacts-json']
604
624
  artifacts.reset(enable=parsed.artifacts_json)
@@ -836,7 +856,9 @@ def error(
836
856
  global max_error_code # pylint: disable=global-statement
837
857
 
838
858
  if start is None:
839
- start = "ERROR: " + (f"[{progname}] " if progname_in_message else "")
859
+ start = f"{Colors.bold}ERROR:{Colors.normal}{Colors.red} "
860
+ if progname_in_message:
861
+ start += f"[{progname}] "
840
862
  start += safe_emoji("❌ ")
841
863
  args['errors'] += 1
842
864
  max_error_code = max(max_error_code, error_code)
@@ -879,7 +901,8 @@ def exit( # pylint: disable=redefined-builtin
879
901
  elif args['warnings']:
880
902
  info_color = Colors.yellow
881
903
  info(
882
- f"{start}Exiting with {args['warnings']} warnings, {args['errors']} errors",
904
+ f"{start}Exiting with {Colors.bold}{args['warnings']} warnings{info_color},",
905
+ f"{Colors.bold}{args['errors']} errors",
883
906
  color=info_color
884
907
  )
885
908
  sys.exit(error_code)
@@ -132,11 +132,11 @@ def get_terminal_columns():
132
132
 
133
133
  Returns:
134
134
  int: The number of columns in the terminal, or a default value (e.g., 80)
135
- if the terminal size cannot be determined.
135
+ if the terminal size cannot be determined. Min value of 40 is returned.
136
136
  """
137
137
  try:
138
138
  size = os.get_terminal_size()
139
- return size.columns
139
+ return max(40, size.columns)
140
140
  except OSError:
141
141
  # Handle cases where the terminal size cannot be determined (e.g., not in a TTY)
142
142
  return 80 # Default to 80 columns
@@ -166,13 +166,13 @@ def pretty_list_columns_manual(data: list, num_columns: int = 4, auto_columns: b
166
166
  max_line_len = 0
167
167
  for x in max_lengths:
168
168
  max_line_len += x + _spacing
169
- if max_line_len > window_cols:
169
+ if max_line_len >= window_cols:
170
170
  # subtract a column (already >= 2):
171
171
  ret_lines.extend(
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)
@@ -6,7 +6,9 @@ import subprocess
6
6
  import sys
7
7
 
8
8
  import psutil
9
+ from opencos import util
9
10
  from opencos.util import debug, error, info, warning, progname, global_log
11
+ from opencos.utils.str_helpers import strip_ansi_color
10
12
 
11
13
  IS_WINDOWS = sys.platform.startswith('win')
12
14
 
@@ -68,7 +70,7 @@ def subprocess_run_background( # pylint: disable=too-many-branches
68
70
 
69
71
  proc_kwargs = {'shell': shell,
70
72
  'stdout': subprocess.PIPE,
71
- 'stderr': subprocess.STDOUT,
73
+ 'stderr': subprocess.STDOUT
72
74
  }
73
75
  if work_dir:
74
76
  proc_kwargs['cwd'] = work_dir
@@ -101,14 +103,29 @@ def subprocess_run_background( # pylint: disable=too-many-branches
101
103
  error(f'Unable to open file "{tee_fpath}" for writing, {e}')
102
104
 
103
105
  for line in iter(proc.stdout.readline, b''):
104
- line = line.rstrip().decode("utf-8", errors="replace")
106
+ line = line.decode("utf-8", errors="replace") # leave \n intact
107
+
108
+ # Since we don't control what the subprocess command did, if it
109
+ # thinks we support color, but user ran with --no-color, we need to strip ANSI colors:
110
+ if not util.args['color']:
111
+ line = strip_ansi_color(line)
112
+
113
+ # Print the line with color, if --color:
105
114
  if not background:
106
- print(line)
115
+ print(line, end='')
116
+
117
+ # for all logs, and the returned stdout str, if we haven't stripped color yet,
118
+ # we need to now, before writing to tee_fpath_f, or to global_log:
119
+ if util.args['color']:
120
+ line = strip_ansi_color(line)
121
+ line = line.replace('\r', '') # remove CR
122
+
107
123
  if tee_fpath_f:
108
- tee_fpath_f.write(line + '\n')
124
+ tee_fpath_f.write(line)
109
125
  if global_log.file:
110
- global_log.write(line, '\n')
111
- stdout += line + '\n'
126
+ # directly write to file handle, avoid util.UtilLogger.write(line, end='')
127
+ global_log.file.write(line)
128
+ stdout += line
112
129
 
113
130
  proc.communicate()
114
131
  remove_completed_parent_pid(proc.pid)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.3.6
3
+ Version: 0.3.8
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
@@ -17,11 +17,13 @@ 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: supports_color>=0.2.0
20
- Requires-Dist: cocotb>=2.0
21
- Requires-Dist: pytest>=8.3.5
22
- Requires-Dist: coverage>=7.6.1
23
20
  Provides-Extra: dev
24
21
  Requires-Dist: pylint>=3.0.0; extra == "dev"
22
+ Requires-Dist: pytest>=8.3.5; extra == "dev"
23
+ Provides-Extra: cocotb
24
+ Requires-Dist: cocotb>=2.0; extra == "cocotb"
25
+ Requires-Dist: pytest>=8.3.5; extra == "cocotb"
26
+ Requires-Dist: coverage>=7.6.1; extra == "cocotb"
25
27
  Provides-Extra: docs
26
28
  Requires-Dist: mkdocs; extra == "docs"
27
29
  Requires-Dist: mkdocs-material; extra == "docs"