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.
opencos/eda_base.py CHANGED
@@ -26,7 +26,7 @@ from opencos import eda_config
26
26
 
27
27
  from opencos.util import Colors, safe_emoji
28
28
  from opencos.utils.str_helpers import sprint_time, strip_outer_quotes, string_or_space, \
29
- indent_wrap_long_text, pretty_list_columns_manual
29
+ indent_wrap_long_text, pretty_list_columns_manual, get_terminal_columns
30
30
  from opencos.utils.subprocess_helpers import subprocess_run_background
31
31
  from opencos.utils import status_constants
32
32
 
@@ -42,6 +42,24 @@ def print_base_help() -> None:
42
42
  print(get_argparser_short_help())
43
43
 
44
44
 
45
+ def print_eda_usage_line(no_targets: bool = False, command_name='COMMAND') -> None:
46
+ '''Prints line for eda [options] COMMAND [options] FILES|TARGETS,...'''
47
+ print(f'{safe_emoji("🔦 ")}Usage:')
48
+ if no_targets:
49
+ print(
50
+ (f' {Colors.bold}{Colors.yellow}eda {Colors.cyan}[options]'
51
+ f' {Colors.yellow}{command_name} {Colors.cyan}[options]{Colors.normal}')
52
+ )
53
+ else:
54
+ print(
55
+ (f' {Colors.bold}{Colors.yellow}eda {Colors.cyan}[options]'
56
+ f' {Colors.yellow}{command_name} {Colors.cyan}[options]'
57
+ f' {Colors.yellow}FILES|TARGETS,...{Colors.normal}')
58
+ )
59
+ print()
60
+
61
+
62
+
45
63
  def get_argparser() -> argparse.ArgumentParser:
46
64
  '''Returns the ArgumentParser for general eda CLI'''
47
65
  parser = argparse.ArgumentParser(
@@ -82,7 +100,7 @@ def get_argparsers_args_list() -> list:
82
100
 
83
101
 
84
102
  def get_eda_exec(command: str = '') -> str:
85
- '''Returns the full path of `eda` executable to be used for a given eda <command>'''
103
+ '''Returns the full path of `eda` executable to be used for a given eda COMMAND'''
86
104
  # NOTE(drew): This is kind of flaky. 'eda multi' reinvokes 'eda'. But the executable for 'eda'
87
105
  # is one of:
88
106
  # 1. pip3 install opencos-eda
@@ -133,6 +151,17 @@ def which_tool(command: str, config: dict) -> str:
133
151
  return tool
134
152
 
135
153
 
154
+ def get_class_tool_name(command_obj: object) -> str:
155
+ '''Attempts to return command_obj._TOOL via command_obj.get_tool_name()'''
156
+ if f := getattr(command_obj, 'get_tool_name', None):
157
+ if callable(f):
158
+ ret = f()
159
+ if ret and isinstance(ret, str):
160
+ return ret
161
+ return ''
162
+
163
+
164
+
136
165
  class Tool:
137
166
  '''opencos.eda_base.Tool is a base class used by opencos.tools.<name>.
138
167
 
@@ -149,6 +178,10 @@ class Tool:
149
178
  # a Command object's self.args instead of the class Tool.args. Safely create it
150
179
  # if it doesn't exist:
151
180
  self._VERSION = None
181
+
182
+ self.tool_warning_count = 0
183
+ self.tool_error_count = 0
184
+
152
185
  if getattr(self, 'args', None) is None:
153
186
  self.args = {}
154
187
  if getattr(self, 'args_help', None) is None:
@@ -182,6 +215,10 @@ class Tool:
182
215
  util.info(f'Override for {self._TOOL} using exe {exe}')
183
216
  self._EXE = exe
184
217
 
218
+ def get_tool_name(self) -> str:
219
+ '''Returns _TOOL'''
220
+ return str(self._TOOL)
221
+
185
222
  def get_full_tool_and_versions(self) -> str:
186
223
  '''Returns tool:version, such as: verilator:5.033'''
187
224
  if not self._VERSION:
@@ -192,13 +229,31 @@ class Tool:
192
229
  '''Sets and returns self._VERSION'''
193
230
  return self._VERSION
194
231
 
232
+ def report_tool_warn_error_counts(self) -> None:
233
+ '''Reports info line based on self.tool_error_count and self.tool_warning_count.'''
234
+ tool_name = get_class_tool_name(self)
235
+ if not tool_name:
236
+ return
237
+
238
+ info_color = Colors.green
239
+ start = ''
240
+ if self.tool_error_count or self.tool_warning_count:
241
+ start = safe_emoji('🔶 ')
242
+ info_color = Colors.yellow
243
+ util.info(
244
+ f"{start}Tool - {tool_name}, total counts:",
245
+ f"{Colors.bold}{self.tool_warning_count} tool warnings{Colors.normal}{info_color},",
246
+ f"{Colors.bold}{self.tool_error_count} tool errors",
247
+ color=info_color
248
+ )
249
+
195
250
  def set_tool_defines(self) -> None:
196
251
  '''Derived classes may override, sets any additional defines based on tool.'''
197
252
  return
198
253
 
199
254
 
200
- class Command: # pylint: disable=too-many-public-methods
201
- '''Base class for all: eda <command>
255
+ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attributes
256
+ '''Base class for all: eda COMMAND
202
257
 
203
258
  The Command class should be used when you don't require files, otherwise consider
204
259
  CommandDesign.
@@ -211,6 +266,8 @@ class Command: # pylint: disable=too-many-public-methods
211
266
  self.args = {}
212
267
  if getattr(self, 'args_help', None) is None:
213
268
  self.args_help = {}
269
+ if getattr(self, 'args_kwargs', None) is None:
270
+ self.args_kwargs = {}
214
271
  self.args.update({
215
272
  "keep" : False,
216
273
  "force" : False,
@@ -235,21 +292,54 @@ class Command: # pylint: disable=too-many-public-methods
235
292
  'error-unknown-args': True,
236
293
  })
237
294
  self.args_help.update({
238
- 'stop-before-compile': ('stop this run before any compile (if possible for tool) and'
239
- ' save .sh scripts in eda-dir/'),
240
- 'eda-dir': 'relative directory where eda logs are saved',
295
+ 'keep': (
296
+ 'Determined by eda <tool>, generally prevents jobs from overwriting artifacts'
297
+ ),
298
+ 'force': 'Determined by eda <tool>, force override',
299
+ 'fake': (
300
+ 'Determined by eda <tool>, generally is a dry-run that does not execute tool'
301
+ ),
302
+ 'stop-before-compile': (
303
+ 'stop this run before any compile steps (if possible for tool) and'
304
+ ' save .sh scripts in eda-dir/'
305
+ ),
306
+ 'stop-after-compile': (
307
+ 'stop this run after any compile steps (if possible for tool) and'
308
+ ' save .sh scripts in eda-dir/'
309
+ ),
310
+ 'stop-after-elaborate': (
311
+ 'stop this run after any elaborate steps (if possible for tool) and'
312
+ ' save .sh scripts in eda-dir/'
313
+ ),
314
+ 'lint': 'tool dependent, run with linting options',
315
+ "job-name" : 'Optional, used to create a sub directory under base work-dir (eda-dir)',
316
+ "sub-work-dir" : (
317
+ 'Optional, similar to job-name, can be used to name the directory created under'
318
+ ' eda-dir'
319
+ ),
320
+ 'eda-dir': 'Optional, relative base directory where eda logs are saved',
241
321
  'export': 'export results for these targets in eda-dir',
242
322
  'export-run': 'export, and run, results for these targets in eda-dir',
243
323
  'export-json': 'export, and save a JSON file per target',
244
- 'work-dir': ('Optional override for working directory, often defaults to'
245
- ' ./eda.work/<top>.<command>'),
324
+ 'work-dir': ('Optional override for working directory, if unset will often use '
325
+ ' ./eda.work/[TOP|TARGET].COMMAND'),
246
326
  "work-dir-use-target-dir": ('Set the work-dir to be the same as the in-place location'
247
327
  ' where the target (DEPS) exists'),
248
- 'enable-tags': ('DEPS markup tag names to be force enabled for this'
249
- ' command (mulitple appends to list).'),
250
- 'diable-tags': ('DEPS markup tag names to be disabled (even if they'
251
- ' match the criteria) for this command (mulitple appends to list).'
252
- ' --disable-tags has higher precedence than --enable-tags.'),
328
+ "suffix": (
329
+ "Optional, determined by eda COMMAND, used by 'multi' jobs information logging"
330
+ ),
331
+ "design": (
332
+ "Optional, used to override both the work-dir, and if unset the top DEPS target"
333
+ ),
334
+ 'enable-tags': (
335
+ 'DEPS markup tag names to be force enabled for this'
336
+ ' command (mulitple appends to list).'
337
+ ),
338
+ 'disable-tags': (
339
+ 'DEPS markup tag names to be disabled (even if they match the criteria) for this'
340
+ ' command (mulitple appends to list). --disable-tags has higher precedence than'
341
+ ' --enable-tags.'
342
+ ),
253
343
  'test-mode': ('command and tool dependent, usually stops the command early without'
254
344
  ' executing.'),
255
345
  'error-unknown-args': (
@@ -266,6 +356,13 @@ class Command: # pylint: disable=too-many-public-methods
266
356
  self.tool_changed_respawn = {}
267
357
 
268
358
 
359
+ def get_info_job_name(self) -> str:
360
+ '''Returns an informational string of the job name, using: command - tool - top'''
361
+ return ' - '.join(
362
+ x for x in (self.command_name, self.args.get('tool', ''),
363
+ self.args.get('top', '')) if x
364
+ )
365
+
269
366
  def error(self, *args, **kwargs) -> None:
270
367
  '''Returns None, child classes can call self.error(..) instead of util.error,
271
368
 
@@ -293,7 +390,6 @@ class Command: # pylint: disable=too-many-public-methods
293
390
  f'ERROR: [eda] ({self.command_name}) {" ".join(list(args))}',
294
391
  file=self.errors_log_f
295
392
  )
296
-
297
393
  self.status = util.error(*args, **kwargs) # error_code passed and returned via kwargs
298
394
 
299
395
  def stop_process_tokens_before_do_it(self) -> bool:
@@ -308,24 +404,28 @@ class Command: # pylint: disable=too-many-public-methods
308
404
  return True
309
405
  return False
310
406
 
311
- def status_any_error(self, report=True) -> bool:
407
+ def status_any_error(self, report: bool = True) -> bool:
312
408
  '''Used by derived classes process_tokens() to know an error was reached
313
- and to not perform the command. Necessary for pytests that use eda.main()'''
314
- if report and self.status > 0:
409
+ and to not perform the command. Necessary for pytests that use eda.main()
410
+
411
+ Note we also check any parent Tool class for tool_error_count > 0
412
+ '''
413
+ any_err = False
414
+ if self.status > 0:
415
+ any_err = True
416
+ elif isinstance(self, Tool) and getattr(self, 'tool_error_count', 0):
417
+ util.warning(f'eda_base.py internal: Command status={self.status}, but',
418
+ f'Tool tool_error_count={getattr(self, "tool_error_count", 0)}')
419
+ any_err = True
420
+ if report and any_err:
315
421
  util.error(f"command '{self.command_name}' has previous errors")
316
- return self.status > 0
422
+ return any_err
317
423
 
318
424
  def report_pass_fail(self) -> None:
319
425
  '''Reports an INFO line with pass/fail information'''
320
- job_name = ' - '.join(
321
- x for x in (self.command_name, self.args.get('tool', ''),
322
- self.args.get('top', '')) if x
323
- )
324
- if self.status_any_error():
325
- util.info(f'{safe_emoji("❌ ")}{job_name}: Errors observed.', color=Colors.red)
326
- else:
327
- util.info(f'{safe_emoji("✅ ")}{job_name}: No errors observed.')
328
-
426
+ job_name = self.get_info_job_name()
427
+ if not self.status_any_error():
428
+ util.info(f'{safe_emoji("✅ ")}{job_name}: {Colors.bold}No errors observed.')
329
429
 
330
430
  def which_tool(self, command:str) -> str:
331
431
  '''Returns a str for the tool name used for the requested command'''
@@ -356,7 +456,7 @@ class Command: # pylint: disable=too-many-public-methods
356
456
  ) -> str:
357
457
  '''Creates the working directory and populates self.args['work-dir']
358
458
 
359
- Generally uses ./ self.args['eda-dir'] / <target-name>.<command> /
459
+ Generally uses ./ self.args['eda-dir'] / TARGET-NAME.COMMAND /
360
460
  however, self.args['job-name'] or ['sub-work-dir'] can override that.
361
461
 
362
462
  Additionally, the work-dir is attempted to be deleted if it already exists
@@ -491,16 +591,17 @@ class Command: # pylint: disable=too-many-public-methods
491
591
  if return_code > 0:
492
592
  if return_code == 1:
493
593
  self.status = status_constants.EDA_EXEC_NONZERO_RETURN_CODE1
494
- if return_code == 255:
594
+ elif return_code == 255:
495
595
  self.status = status_constants.EDA_EXEC_NONZERO_RETURN_CODE255
496
596
  else:
497
597
  self.status = status_constants.EDA_EXEC_NONZERO_RETURN_CODE2
598
+
498
599
  if stop_on_error:
499
600
  self.error(f"exec: returned with error (return code: {return_code})",
500
601
  error_code=self.status)
501
602
  else:
502
- util.debug(f"{safe_emoji('❌ ')}exec: returned with error (return code:",
503
- f"{return_code})")
603
+ util.info(f"{safe_emoji('❌ ')}exec: returned with error (return code:",
604
+ f"{return_code})")
504
605
  else:
505
606
  util.debug(f"exec: returned without error (return code: {return_code})")
506
607
  return stderr, stdout, return_code
@@ -614,25 +715,42 @@ class Command: # pylint: disable=too-many-public-methods
614
715
  help_kwargs = {'help': f'{type(value).__name__} default={value}'}
615
716
  help_kwargs['help'] = help_kwargs['help'].replace('%', '%%')
616
717
 
718
+ # Update with any self.args_kwargs for this key
719
+ if self.args_kwargs.get(key, {}):
720
+ help_kwargs.update(self.args_kwargs.get(key, {}))
721
+
617
722
 
618
723
  # It's important to set the default=None on these, except for list types where default
619
724
  # is []. If the parsed Namespace has values set to None or [], we do not update. This
620
725
  # means that as deps are processed that have args set, they cannot override the top
621
726
  # level args that were already set, nor be overriden by defaults.
622
- if isinstance(value, bool):
623
- # For bool, support --key and --no-key with action=argparse.BooleanOptionalAction.
624
- # Note, this means you cannot use --some-bool=True, or --some-bool=False, has to
625
- # be --some-bool or --no-some-bool.
626
- parser.add_argument(
627
- *arguments, default=None, **bool_action_kwargs, **help_kwargs)
628
- elif isinstance(value, (list, set)):
629
- parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
630
- elif isinstance(value, (int, str)):
631
- parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
632
- elif value is None:
633
- parser.add_argument(*arguments, default=None, **help_kwargs)
634
- else:
635
- assert False, f'{key=} {value=} how do we do argparse for this type of value?'
727
+ try:
728
+ if isinstance(value, bool):
729
+ # For bool, support --key and --no-key with
730
+ # action=argparse.BooleanOptionalAction. Note, this means you cannot use
731
+ # --some-bool=True, or --some-bool=False, has to be --some-bool or
732
+ # --no-some-bool.
733
+ # Also we cannot have self.args.keys() that start with 'no-', which is
734
+ # why this entire thing is wrapped with try/except.
735
+ parser.add_argument(
736
+ *arguments, default=None, **bool_action_kwargs, **help_kwargs)
737
+ elif isinstance(value, (list, set)):
738
+ parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
739
+ elif isinstance(value, (int, str)):
740
+ parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
741
+ elif value is None:
742
+ parser.add_argument(*arguments, default=None, **help_kwargs)
743
+ else:
744
+ assert False, f'{key=} {value=} how do we do argparse for this type of value?'
745
+ except Exception as e:
746
+ if isinstance(value, bool):
747
+ self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
748
+ f'{arguments=} {bool_action_kwargs=} {help_kwargs=} {e=}')
749
+ else:
750
+ self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
751
+ f'{arguments=} {help_kwargs=} {e=}')
752
+
753
+
636
754
 
637
755
  return parser
638
756
 
@@ -730,7 +848,7 @@ class Command: # pylint: disable=too-many-public-methods
730
848
  def get_command_from_unparsed_args(
731
849
  self, tokens: list, error_if_no_command: bool = True
732
850
  ) -> str:
733
- '''Given a list of unparsed args, try to fish out the eda <command> value.
851
+ '''Given a list of unparsed args, try to fish out the eda COMMAND value.
734
852
 
735
853
  This will remove the value from the tokens list.
736
854
  '''
@@ -742,7 +860,7 @@ class Command: # pylint: disable=too-many-public-methods
742
860
  break
743
861
 
744
862
  if not ret and error_if_no_command:
745
- self.error(f"Looking for a valid eda {self.command_name} <command>",
863
+ self.error(f"Looking for a valid eda {self.command_name} COMMAND",
746
864
  f"but didn't find one in {tokens=}",
747
865
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
748
866
  return ret
@@ -808,19 +926,16 @@ class Command: # pylint: disable=too-many-public-methods
808
926
  if self.args_help has additional help information.
809
927
  '''
810
928
 
811
- # Indent long lines (>100) to indent=56 (this is where we leave off w/ {vstr:12} below.
812
- def indent_me(text:str):
813
- return indent_wrap_long_text(text, width=100, indent=56)
929
+ # Indent long lines (>100) to indent=24 (this is where usual argparse help leaves off)
930
+ def indent_me(text: str, initial_indent: int = 23):
931
+ return indent_wrap_long_text(
932
+ ' ' * initial_indent + text, width=get_terminal_columns(), indent=24
933
+ )
814
934
 
815
- util.info('Help:')
935
+ util.info('Help:', color=Colors.cyan)
816
936
  # using bare 'print' here, since help was requested, avoids --color and --quiet
817
937
  print()
818
- print(f'{safe_emoji("🔦 ")}Usage:')
819
- if no_targets:
820
- print(f' eda [options] {self.command_name} [options]')
821
- else:
822
- print(f' eda [options] {self.command_name} [options] [files|targets, ...]')
823
- print()
938
+ print_eda_usage_line(no_targets=no_targets, command_name=self.command_name)
824
939
 
825
940
  print_base_help()
826
941
  lines = []
@@ -829,10 +944,18 @@ class Command: # pylint: disable=too-many-public-methods
829
944
  return
830
945
 
831
946
  if self.command_name:
832
- lines.append(f"{safe_emoji('🔧 ')}Generic help for command='{self.command_name}'"
833
- f" (using '{self.__class__.__name__}')")
947
+ line = (
948
+ f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help for"
949
+ f" command={Colors.byellow}{self.command_name}{Colors.cyan}"
950
+ )
951
+ if tool := get_class_tool_name(self):
952
+ line += f" tool={Colors.byellow}{tool}"
953
+ line += f" {Colors.normal}(using '{self.__class__.__name__}')"
954
+ lines.append(line)
834
955
  else:
835
- lines.append("{safe_emoji('🔧 ')}Generic help (from class Command):")
956
+ lines.append(
957
+ f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help:{Colors.normal}"
958
+ )
836
959
 
837
960
  # Attempt to run argparser on args, but don't error if it fails.
838
961
  unparsed = []
@@ -842,44 +965,46 @@ class Command: # pylint: disable=too-many-public-methods
842
965
  except Exception:
843
966
  pass
844
967
 
845
- for k in sorted(self.args.keys()):
846
- v = self.args[k]
847
- vstr = str(v)
848
- khelp = self.args_help.get(k, '')
849
- if khelp:
850
- khelp = f' - {khelp}'
851
- if isinstance(v, bool):
852
- lines.append(indent_me(f" --{k:20} : boolean : {vstr:12}{khelp}"))
853
- elif isinstance(v, int):
854
- lines.append(indent_me(f" --{k:20} : integer : {vstr:12}{khelp}"))
855
- elif isinstance(v, list):
856
- lines.append(indent_me(f" --{k:20} : list : {vstr:12}{khelp}"))
857
- elif isinstance(v, str):
858
- vstr = "'" + v + "'"
859
- lines.append(indent_me(f" --{k:20} : string : {vstr:12}{khelp}"))
860
- else:
861
- lines.append(indent_me(f" --{k:20} : <unknown> : {vstr:12}{khelp}"))
968
+ short_help_lines = util.get_argparser_short_help(
969
+ parser=self.get_argparser(support_underscores=False)
970
+ ).split('\n')
971
+ short_help_lines.pop(0) # Strip first line w/ argparser prog name
972
+ lines.extend(short_help_lines)
973
+
974
+ # For these custom args, -GParameter=Value, +define+Name[=value] +incdir+path
975
+ # make colors to look like python >= 3.14, if that is our version.
976
+ color = Colors()
977
+ if sys.version_info.major < 3 or \
978
+ (sys.version_info.major == 3 and sys.version_info.minor < 14):
979
+ color.disable() # strip our color object if < 3.14
980
+
862
981
 
863
982
  lines.append('')
983
+ lines.append(
984
+ f" {color.cyan}-G{color.byellow}<parameterName>{color.normal}=" \
985
+ + f"{color.yellow}<value>{color.normal}"
986
+ )
864
987
  lines.append(indent_me((
865
- " -G<parameterName>=<value> "
866
988
  " Add parameter to top level, support bit/int/string types only."
867
989
  " Example: -GDEPTH=8 (DEPTH treated as SV int/integer)."
868
990
  " -GENABLE=1 (ENABLED treated as SV bit/int/integer)."
869
991
  " -GName=eda (Name treated as SV string \"eda\")."
870
992
  )))
993
+
994
+ lines.append(f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}")
871
995
  lines.append(indent_me((
872
- " +define+<defineName> "
873
996
  " Add define w/out value to tool ahead of SV sources"
874
997
  " Example: +define+SIM_SPEEDUP"
875
998
  )))
999
+ lines.append(
1000
+ f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}=" \
1001
+ + f"{color.yellow}<value>{color.normal}")
876
1002
  lines.append(indent_me((
877
- " +define+<defineName>=<value> "
878
1003
  " Add define w/ value to tool ahead of SV sources"
879
1004
  " Example: +define+TECH_LIB=2 +define+FULL_NAME=\"E D A\""
880
1005
  )))
1006
+ lines.append(f" {color.cyan}+incdir+{color.byellow}PATH{color.normal}")
881
1007
  lines.append(indent_me((
882
- " +incdir+<path> "
883
1008
  " Add path (absolute or relative) for include directories"
884
1009
  " for SystemVerilog `include \"<some-file>.svh\""
885
1010
  " Example: +incdir+../lib"
@@ -954,6 +1079,26 @@ class Command: # pylint: disable=too-many-public-methods
954
1079
  util.warning(*msg)
955
1080
 
956
1081
 
1082
+ def update_tool_warn_err_counts_from_log_lines(
1083
+ self, log_lines: list, bad_strings: list, warning_strings: list
1084
+ ) -> None:
1085
+ '''Given lines (list of str) from a log, update self.tool_[error|warning]_count values
1086
+
1087
+ Since Tool class is not always a parent in Command/CommandDesign/CommandSim, we
1088
+ have to modfiy these member names safely for pylint.
1089
+
1090
+ Derived classes may override this, especially if the Tool is known and reports
1091
+ the complete error/warning summary counts in a certain way.
1092
+ '''
1093
+ if not isinstance(self, Tool):
1094
+ return
1095
+ for line in log_lines:
1096
+ if any(bad_str in line for bad_str in bad_strings):
1097
+ setattr(self, 'tool_error_count', getattr(self, 'tool_error_count', 0) + 1)
1098
+ if any(warn_str in line for warn_str in warning_strings):
1099
+ setattr(self, 'tool_warning_count', getattr(self, 'tool_warning_count', 0) + 1)
1100
+
1101
+
957
1102
  class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
958
1103
  '''CommandDesign is the eda base class for command handlers that need to track files.
959
1104
 
opencos/eda_config.py CHANGED
@@ -57,6 +57,7 @@ class Defaults:
57
57
  'defines',
58
58
  'log-bad-strings',
59
59
  'log-must-strings',
60
+ 'log-warning-strings',
60
61
  'sim-libraries',
61
62
  'compile-args',
62
63
  'compile-waves-args',
@@ -154,6 +154,8 @@ tools:
154
154
 
155
155
  verilator:
156
156
  defines: { }
157
+ log-warning-strings:
158
+ - "%Warning"
157
159
  log-bad-strings:
158
160
  - "%Error"
159
161
  - "%Fatal"
@@ -239,8 +241,11 @@ tools:
239
241
  defines:
240
242
  OC_TOOL_RIVIERA: 1
241
243
  RIVIERA: 1
244
+ log-warning-strings:
245
+ - "Warning: "
242
246
  log-bad-strings:
243
- - "Error:"
247
+ - "Error: "
248
+ - "Fatal: "
244
249
  log-must-strings:
245
250
  - "VSIM: Simulation has finished"
246
251
  compile-args: |
@@ -261,8 +266,11 @@ tools:
261
266
  defines:
262
267
  OC_ASSERT_PROPERTY_NOT_SUPPORTED: 1
263
268
  OC_TOOL_MODELSIM_ASE: 1
269
+ log-warning-strings:
270
+ - "Warning: "
264
271
  log-bad-strings:
265
- - "Error:"
272
+ - "Error: "
273
+ - "Fatal: "
266
274
  log-must-strings:
267
275
  - " vsim "
268
276
  - "Errors: 0"
@@ -286,8 +294,11 @@ tools:
286
294
  questa_fse:
287
295
  defines:
288
296
  OC_TOOL_QUESTA_FSE: 1
297
+ log-warning-strings:
298
+ - "Warning: "
289
299
  log-bad-strings:
290
- - "Error:"
300
+ - "Error: "
301
+ - "Fatal: "
291
302
  log-must-strings:
292
303
  - " vsim "
293
304
  - "Errors: 0"
@@ -309,6 +320,9 @@ tools:
309
320
 
310
321
 
311
322
  iverilog:
323
+ log-warning-strings:
324
+ - "Warning:"
325
+ - "WARNING:"
312
326
  log-bad-strings:
313
327
  - "Error:"
314
328
  - "ERROR:"
@@ -328,6 +342,9 @@ tools:
328
342
  cocotb:
329
343
  defines:
330
344
  OC_TOOL_COCOTB: null
345
+ log-warning-strings:
346
+ - "Warning:"
347
+ - "WARNING:"
331
348
  log-bad-strings:
332
349
  - "ERROR"
333
350
  - "FAILED"
@@ -344,6 +361,14 @@ tools:
344
361
  OC_TOOL_QUARTUS: null
345
362
 
346
363
  vivado:
364
+ log-warning-strings:
365
+ - "WARNING: "
366
+ - "Warning: "
367
+ log-bad-strings:
368
+ - "FATAL: "
369
+ - "Fatal: "
370
+ - "ERROR: "
371
+ - "Error: "
347
372
  sim-libraries:
348
373
  - xil_defaultlib
349
374
  - unisims_ver
@@ -29,3 +29,16 @@ target_test_with_post_tool_commands:
29
29
  run-after-tool: true
30
30
  - target_echo_hi_bye
31
31
  top: foo
32
+
33
+
34
+ test_run_from_work_dir:
35
+ deps:
36
+ - commands:
37
+ - shell: echo "pwd=$PWD"
38
+ run-from-work-dir: true
39
+
40
+ test_run_from_work_dir_false:
41
+ deps:
42
+ - commands:
43
+ - shell: echo "pwd=$PWD"
44
+ run-from-work-dir: false
opencos/tests/helpers.py CHANGED
@@ -10,8 +10,8 @@ from pathlib import Path
10
10
  from contextlib import redirect_stdout, redirect_stderr
11
11
 
12
12
  from opencos import eda, eda_tool_helper, deps_schema
13
- from opencos.utils.markup_helpers import yaml_safe_load
14
13
  from opencos.utils import status_constants
14
+ from opencos.utils.markup_helpers import yaml_safe_load
15
15
  from opencos.utils.subprocess_helpers import subprocess_run_background
16
16
 
17
17
  # Figure out what tools the system has available, without calling eda.main(..)
@@ -238,7 +238,7 @@ class Helpers:
238
238
 
239
239
  def log_it(
240
240
  self, command_str: str, logfile=None, use_eda_wrap: bool = True,
241
- run_in_subprocess: bool = False,
241
+ run_in_subprocess: bool = False, include_default_log: bool = False,
242
242
  preserve_env: bool = False
243
243
  ) -> int:
244
244
  '''Replacement for calling eda.main or eda_wrap, when you want an internal logfile
@@ -257,13 +257,17 @@ class Helpers:
257
257
  logfile = self._resolve_logfile(logfile)
258
258
  rc = 50
259
259
 
260
+ eda_log_arg = '--no-default-log'
261
+ if include_default_log:
262
+ eda_log_arg = ''
263
+
260
264
  # TODO(drew): There are some issues with log_it redirecting stdout from vivado
261
265
  # and modelsim_ase. So this may not work for all tools, you may have to directly
262
266
  # look at eda.work/{target}.sim/sim.log or xsim.log.
263
267
  print(f'{os.getcwd()=}')
264
268
  print(f'{command_str=}')
265
269
  if run_in_subprocess or self.RUN_IN_SUBPROCESS:
266
- command_list = ['eda', '--no-default-log'] + command_str.split()
270
+ command_list = ['eda', eda_log_arg] + command_str.split()
267
271
  _, _, rc = subprocess_run_background(
268
272
  work_dir=self.DEFAULT_DIR,
269
273
  command_list=command_list,
@@ -275,9 +279,9 @@ class Helpers:
275
279
  with open(logfile, 'w', encoding='utf-8') as f:
276
280
  with redirect_stdout(f), redirect_stderr(f):
277
281
  if use_eda_wrap or self.USE_EDA_WRAP:
278
- rc = eda_wrap('--no-default-log', *(command_str.split()))
282
+ rc = eda_wrap(eda_log_arg, *(command_str.split()))
279
283
  else:
280
- rc = eda.main('--no-default-log', *(command_str.split()))
284
+ rc = eda.main(eda_log_arg, *(command_str.split()))
281
285
  print(f'Wrote: {os.path.abspath(logfile)=}')
282
286
 
283
287
  if self.PRESERVE_ENV or preserve_env:
opencos/tests/test_eda.py CHANGED
@@ -648,7 +648,7 @@ class TestsRequiresIVerilog(Helpers):
648
648
  print(f'{rc=}')
649
649
  assert rc == 0
650
650
  assert self.is_in_log('Detected iverilog')
651
- assert self.is_in_log("Generic help for command='sim' (using 'CommandSimIverilog')")
651
+ assert self.is_in_log("Generic help for command=sim tool=iverilog")
652
652
 
653
653
  def test_iverilog_sim(self):
654
654
  '''Test for command sim'''