opencos-eda 0.3.5__py3-none-any.whl → 0.3.7__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
@@ -24,9 +24,9 @@ from pathlib import Path
24
24
  from opencos import seed, util, files
25
25
  from opencos import eda_config
26
26
 
27
- from opencos.util import Colors
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,9 +42,29 @@ 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
- parser = argparse.ArgumentParser(prog='eda options', add_help=False, allow_abbrev=False)
65
+ parser = argparse.ArgumentParser(
66
+ prog=f'{safe_emoji("🔎 ")}eda options', add_help=False, allow_abbrev=False
67
+ )
48
68
  parser.add_argument('-q', '--quit', action='store_true',
49
69
  help=(
50
70
  'For interactive mode (eda called with no options, command, or'
@@ -80,7 +100,7 @@ def get_argparsers_args_list() -> list:
80
100
 
81
101
 
82
102
  def get_eda_exec(command: str = '') -> str:
83
- '''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'''
84
104
  # NOTE(drew): This is kind of flaky. 'eda multi' reinvokes 'eda'. But the executable for 'eda'
85
105
  # is one of:
86
106
  # 1. pip3 install opencos-eda
@@ -131,6 +151,17 @@ def which_tool(command: str, config: dict) -> str:
131
151
  return tool
132
152
 
133
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
+
134
165
  class Tool:
135
166
  '''opencos.eda_base.Tool is a base class used by opencos.tools.<name>.
136
167
 
@@ -180,6 +211,10 @@ class Tool:
180
211
  util.info(f'Override for {self._TOOL} using exe {exe}')
181
212
  self._EXE = exe
182
213
 
214
+ def get_tool_name(self) -> str:
215
+ '''Returns _TOOL'''
216
+ return str(self._TOOL)
217
+
183
218
  def get_full_tool_and_versions(self) -> str:
184
219
  '''Returns tool:version, such as: verilator:5.033'''
185
220
  if not self._VERSION:
@@ -196,7 +231,7 @@ class Tool:
196
231
 
197
232
 
198
233
  class Command: # pylint: disable=too-many-public-methods
199
- '''Base class for all: eda <command>
234
+ '''Base class for all: eda COMMAND
200
235
 
201
236
  The Command class should be used when you don't require files, otherwise consider
202
237
  CommandDesign.
@@ -233,21 +268,54 @@ class Command: # pylint: disable=too-many-public-methods
233
268
  'error-unknown-args': True,
234
269
  })
235
270
  self.args_help.update({
236
- 'stop-before-compile': ('stop this run before any compile (if possible for tool) and'
237
- ' save .sh scripts in eda-dir/'),
238
- 'eda-dir': 'relative directory where eda logs are saved',
271
+ 'keep': (
272
+ 'Determined by eda <tool>, generally prevents jobs from overwriting artifacts'
273
+ ),
274
+ 'force': 'Determined by eda <tool>, force override',
275
+ 'fake': (
276
+ 'Determined by eda <tool>, generally is a dry-run that does not execute tool'
277
+ ),
278
+ 'stop-before-compile': (
279
+ 'stop this run before any compile steps (if possible for tool) and'
280
+ ' save .sh scripts in eda-dir/'
281
+ ),
282
+ 'stop-after-compile': (
283
+ 'stop this run after any compile steps (if possible for tool) and'
284
+ ' save .sh scripts in eda-dir/'
285
+ ),
286
+ 'stop-after-elaborate': (
287
+ 'stop this run after any elaborate steps (if possible for tool) and'
288
+ ' save .sh scripts in eda-dir/'
289
+ ),
290
+ 'lint': 'tool dependent, run with linting options',
291
+ "job-name" : 'Optional, used to create a sub directory under base work-dir (eda-dir)',
292
+ "sub-work-dir" : (
293
+ 'Optional, similar to job-name, can be used to name the directory created under'
294
+ ' eda-dir'
295
+ ),
296
+ 'eda-dir': 'Optional, relative base directory where eda logs are saved',
239
297
  'export': 'export results for these targets in eda-dir',
240
298
  'export-run': 'export, and run, results for these targets in eda-dir',
241
299
  'export-json': 'export, and save a JSON file per target',
242
- 'work-dir': ('Optional override for working directory, often defaults to'
243
- ' ./eda.work/<top>.<command>'),
300
+ 'work-dir': ('Optional override for working directory, if unset will often use '
301
+ ' ./eda.work/[TOP|TARGET].COMMAND'),
244
302
  "work-dir-use-target-dir": ('Set the work-dir to be the same as the in-place location'
245
303
  ' where the target (DEPS) exists'),
246
- 'enable-tags': ('DEPS markup tag names to be force enabled for this'
247
- ' command (mulitple appends to list).'),
248
- 'diable-tags': ('DEPS markup tag names to be disabled (even if they'
249
- ' match the criteria) for this command (mulitple appends to list).'
250
- ' --disable-tags has higher precedence than --enable-tags.'),
304
+ "suffix": (
305
+ "Optional, determined by eda COMMAND, used by 'multi' jobs information logging"
306
+ ),
307
+ "design": (
308
+ "Optional, used to override both the work-dir, and if unset the top DEPS target"
309
+ ),
310
+ 'enable-tags': (
311
+ 'DEPS markup tag names to be force enabled for this'
312
+ ' command (mulitple appends to list).'
313
+ ),
314
+ 'disable-tags': (
315
+ 'DEPS markup tag names to be disabled (even if they match the criteria) for this'
316
+ ' command (mulitple appends to list). --disable-tags has higher precedence than'
317
+ ' --enable-tags.'
318
+ ),
251
319
  'test-mode': ('command and tool dependent, usually stops the command early without'
252
320
  ' executing.'),
253
321
  'error-unknown-args': (
@@ -313,6 +381,18 @@ class Command: # pylint: disable=too-many-public-methods
313
381
  util.error(f"command '{self.command_name}' has previous errors")
314
382
  return self.status > 0
315
383
 
384
+ def report_pass_fail(self) -> None:
385
+ '''Reports an INFO line with pass/fail information'''
386
+ job_name = ' - '.join(
387
+ x for x in (self.command_name, self.args.get('tool', ''),
388
+ self.args.get('top', '')) if x
389
+ )
390
+ if self.status_any_error():
391
+ util.info(f'{safe_emoji("❌ ")}{job_name}: Errors observed.', color=Colors.red)
392
+ else:
393
+ util.info(f'{safe_emoji("✅ ")}{job_name}: No errors observed.')
394
+
395
+
316
396
  def which_tool(self, command:str) -> str:
317
397
  '''Returns a str for the tool name used for the requested command'''
318
398
  return which_tool(command, config=self.config)
@@ -342,7 +422,7 @@ class Command: # pylint: disable=too-many-public-methods
342
422
  ) -> str:
343
423
  '''Creates the working directory and populates self.args['work-dir']
344
424
 
345
- Generally uses ./ self.args['eda-dir'] / <target-name>.<command> /
425
+ Generally uses ./ self.args['eda-dir'] / TARGET-NAME.COMMAND /
346
426
  however, self.args['job-name'] or ['sub-work-dir'] can override that.
347
427
 
348
428
  Additionally, the work-dir is attempted to be deleted if it already exists
@@ -462,7 +542,8 @@ class Command: # pylint: disable=too-many-public-methods
462
542
  if not tee_fpath and getattr(command_list, 'tee_fpath', None):
463
543
  tee_fpath = getattr(command_list, 'tee_fpath', '')
464
544
  if not quiet:
465
- util.info(f"exec: {' '.join(command_list)} (in {work_dir}, {tee_fpath=})")
545
+ util.info(f"{safe_emoji('⏩ ')}exec: {' '.join(command_list)}",
546
+ f"(in {work_dir}, {tee_fpath=})")
466
547
 
467
548
  stdout, stderr, return_code = subprocess_run_background(
468
549
  work_dir=work_dir,
@@ -484,7 +565,8 @@ class Command: # pylint: disable=too-many-public-methods
484
565
  self.error(f"exec: returned with error (return code: {return_code})",
485
566
  error_code=self.status)
486
567
  else:
487
- util.debug(f"exec: returned with error (return code: {return_code})")
568
+ util.debug(f"{safe_emoji('❌ ')}exec: returned with error (return code:",
569
+ f"{return_code})")
488
570
  else:
489
571
  util.debug(f"exec: returned without error (return code: {return_code})")
490
572
  return stderr, stdout, return_code
@@ -564,7 +646,9 @@ class Command: # pylint: disable=too-many-public-methods
564
646
  # parsed.args-with-dashes is not legal python. Some of self.args.keys() still have - or _,
565
647
  # so this will handle both.
566
648
  # Also, preference is for self.args.keys(), to be str with - dashes
567
- parser = argparse.ArgumentParser(prog='eda', add_help=False, allow_abbrev=False)
649
+ parser = argparse.ArgumentParser(
650
+ prog=f'{safe_emoji("🔎 ")}eda', add_help=False, allow_abbrev=False
651
+ )
568
652
  bool_action_kwargs = util.get_argparse_bool_action_kwargs()
569
653
 
570
654
  if not parser_arg_list:
@@ -601,20 +685,33 @@ class Command: # pylint: disable=too-many-public-methods
601
685
  # is []. If the parsed Namespace has values set to None or [], we do not update. This
602
686
  # means that as deps are processed that have args set, they cannot override the top
603
687
  # level args that were already set, nor be overriden by defaults.
604
- if isinstance(value, bool):
605
- # For bool, support --key and --no-key with action=argparse.BooleanOptionalAction.
606
- # Note, this means you cannot use --some-bool=True, or --some-bool=False, has to
607
- # be --some-bool or --no-some-bool.
608
- parser.add_argument(
609
- *arguments, default=None, **bool_action_kwargs, **help_kwargs)
610
- elif isinstance(value, (list, set)):
611
- parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
612
- elif isinstance(value, (int, str)):
613
- parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
614
- elif value is None:
615
- parser.add_argument(*arguments, default=None, **help_kwargs)
616
- else:
617
- assert False, f'{key=} {value=} how do we do argparse for this type of value?'
688
+ try:
689
+ if isinstance(value, bool):
690
+ # For bool, support --key and --no-key with
691
+ # action=argparse.BooleanOptionalAction. Note, this means you cannot use
692
+ # --some-bool=True, or --some-bool=False, has to be --some-bool or
693
+ # --no-some-bool.
694
+ # Also we cannot have self.args.keys() that start with 'no-', which is
695
+ # why this entire thing is wrapped with try/except.
696
+ parser.add_argument(
697
+ *arguments, default=None, **bool_action_kwargs, **help_kwargs)
698
+ elif isinstance(value, (list, set)):
699
+ parser.add_argument(*arguments, default=value, action='append', **help_kwargs)
700
+ elif isinstance(value, (int, str)):
701
+ parser.add_argument(*arguments, default=value, type=type(value), **help_kwargs)
702
+ elif value is None:
703
+ parser.add_argument(*arguments, default=None, **help_kwargs)
704
+ else:
705
+ assert False, f'{key=} {value=} how do we do argparse for this type of value?'
706
+ except Exception as e:
707
+ if isinstance(value, bool):
708
+ self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
709
+ f'{arguments=} {bool_action_kwargs=} {help_kwargs=} {e=}')
710
+ else:
711
+ self.error(f'Could not add argument: {key=} {value=} {type(value)=}',
712
+ f'{arguments=} {help_kwargs=} {e=}')
713
+
714
+
618
715
 
619
716
  return parser
620
717
 
@@ -712,7 +809,7 @@ class Command: # pylint: disable=too-many-public-methods
712
809
  def get_command_from_unparsed_args(
713
810
  self, tokens: list, error_if_no_command: bool = True
714
811
  ) -> str:
715
- '''Given a list of unparsed args, try to fish out the eda <command> value.
812
+ '''Given a list of unparsed args, try to fish out the eda COMMAND value.
716
813
 
717
814
  This will remove the value from the tokens list.
718
815
  '''
@@ -724,7 +821,7 @@ class Command: # pylint: disable=too-many-public-methods
724
821
  break
725
822
 
726
823
  if not ret and error_if_no_command:
727
- self.error(f"Looking for a valid eda {self.command_name} <command>",
824
+ self.error(f"Looking for a valid eda {self.command_name} COMMAND",
728
825
  f"but didn't find one in {tokens=}",
729
826
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
730
827
  return ret
@@ -790,19 +887,16 @@ class Command: # pylint: disable=too-many-public-methods
790
887
  if self.args_help has additional help information.
791
888
  '''
792
889
 
793
- # Indent long lines (>100) to indent=56 (this is where we leave off w/ {vstr:12} below.
794
- def indent_me(text:str):
795
- return indent_wrap_long_text(text, width=100, indent=56)
890
+ # Indent long lines (>100) to indent=24 (this is where usual argparse help leaves off)
891
+ def indent_me(text: str, initial_indent: int = 23):
892
+ return indent_wrap_long_text(
893
+ ' ' * initial_indent + text, width=get_terminal_columns(), indent=24
894
+ )
796
895
 
797
- util.info('Help:')
896
+ util.info('Help:', color=Colors.cyan)
798
897
  # using bare 'print' here, since help was requested, avoids --color and --quiet
799
898
  print()
800
- print('Usage:')
801
- if no_targets:
802
- print(f' eda [options] {self.command_name} [options]')
803
- else:
804
- print(f' eda [options] {self.command_name} [options] [files|targets, ...]')
805
- print()
899
+ print_eda_usage_line(no_targets=no_targets, command_name=self.command_name)
806
900
 
807
901
  print_base_help()
808
902
  lines = []
@@ -811,10 +905,18 @@ class Command: # pylint: disable=too-many-public-methods
811
905
  return
812
906
 
813
907
  if self.command_name:
814
- lines.append(f"Generic help for command='{self.command_name}'"
815
- f" (using '{self.__class__.__name__}')")
908
+ line = (
909
+ f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help for"
910
+ f" command={Colors.byellow}{self.command_name}{Colors.cyan}"
911
+ )
912
+ if tool := get_class_tool_name(self):
913
+ line += f" tool={Colors.byellow}{tool}"
914
+ line += f" {Colors.normal}(using '{self.__class__.__name__}')"
915
+ lines.append(line)
816
916
  else:
817
- lines.append("Generic help (from class Command):")
917
+ lines.append(
918
+ f"{safe_emoji('🔧 ')}{Colors.cyan}Generic help:{Colors.normal}"
919
+ )
818
920
 
819
921
  # Attempt to run argparser on args, but don't error if it fails.
820
922
  unparsed = []
@@ -824,44 +926,46 @@ class Command: # pylint: disable=too-many-public-methods
824
926
  except Exception:
825
927
  pass
826
928
 
827
- for k in sorted(self.args.keys()):
828
- v = self.args[k]
829
- vstr = str(v)
830
- khelp = self.args_help.get(k, '')
831
- if khelp:
832
- khelp = f' - {khelp}'
833
- if isinstance(v, bool):
834
- lines.append(indent_me(f" --{k:20} : boolean : {vstr:12}{khelp}"))
835
- elif isinstance(v, int):
836
- lines.append(indent_me(f" --{k:20} : integer : {vstr:12}{khelp}"))
837
- elif isinstance(v, list):
838
- lines.append(indent_me(f" --{k:20} : list : {vstr:12}{khelp}"))
839
- elif isinstance(v, str):
840
- vstr = "'" + v + "'"
841
- lines.append(indent_me(f" --{k:20} : string : {vstr:12}{khelp}"))
842
- else:
843
- lines.append(indent_me(f" --{k:20} : <unknown> : {vstr:12}{khelp}"))
929
+ short_help_lines = util.get_argparser_short_help(
930
+ parser=self.get_argparser(support_underscores=False)
931
+ ).split('\n')
932
+ short_help_lines.pop(0) # Strip first line w/ argparser prog name
933
+ lines.extend(short_help_lines)
934
+
935
+ # For these custom args, -GParameter=Value, +define+Name[=value] +incdir+path
936
+ # make colors to look like python >= 3.14, if that is our version.
937
+ color = Colors()
938
+ if sys.version_info.major < 3 or \
939
+ (sys.version_info.major == 3 and sys.version_info.minor < 14):
940
+ color.disable() # strip our color object if < 3.14
941
+
844
942
 
845
943
  lines.append('')
944
+ lines.append(
945
+ f" {color.cyan}-G{color.byellow}<parameterName>{color.normal}=" \
946
+ + f"{color.yellow}<value>{color.normal}"
947
+ )
846
948
  lines.append(indent_me((
847
- " -G<parameterName>=<value> "
848
949
  " Add parameter to top level, support bit/int/string types only."
849
950
  " Example: -GDEPTH=8 (DEPTH treated as SV int/integer)."
850
951
  " -GENABLE=1 (ENABLED treated as SV bit/int/integer)."
851
952
  " -GName=eda (Name treated as SV string \"eda\")."
852
953
  )))
954
+
955
+ lines.append(f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}")
853
956
  lines.append(indent_me((
854
- " +define+<defineName> "
855
957
  " Add define w/out value to tool ahead of SV sources"
856
958
  " Example: +define+SIM_SPEEDUP"
857
959
  )))
960
+ lines.append(
961
+ f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}=" \
962
+ + f"{color.yellow}<value>{color.normal}")
858
963
  lines.append(indent_me((
859
- " +define+<defineName>=<value> "
860
964
  " Add define w/ value to tool ahead of SV sources"
861
965
  " Example: +define+TECH_LIB=2 +define+FULL_NAME=\"E D A\""
862
966
  )))
967
+ lines.append(f" {color.cyan}+incdir+{color.byellow}PATH{color.normal}")
863
968
  lines.append(indent_me((
864
- " +incdir+<path> "
865
969
  " Add path (absolute or relative) for include directories"
866
970
  " for SystemVerilog `include \"<some-file>.svh\""
867
971
  " Example: +incdir+../lib"
@@ -980,6 +1084,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
980
1084
  self.files_vhd = []
981
1085
  self.files_cpp = []
982
1086
  self.files_sdc = []
1087
+ self.files_py = []
1088
+ self.files_makefile = []
983
1089
  self.files_non_source = []
984
1090
  self.files_caller_info = {}
985
1091
  self.dep_shell_commands = [] # each list entry is a {}
@@ -1139,7 +1245,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1139
1245
  self.files[new_key] = True
1140
1246
 
1141
1247
  my_file_lists_list = [self.files_v, self.files_sv, self.files_vhd, self.files_cpp,
1142
- self.files_sdc]
1248
+ self.files_sdc, self.files_py, self.files_makefile]
1143
1249
  for my_file_list in my_file_lists_list:
1144
1250
  for i,value in enumerate(my_file_list):
1145
1251
  if value and isinstance(value, str) and \
@@ -1157,9 +1263,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1157
1263
  need to be copied or linked to the work-dir. For example, if some SV assumes it
1158
1264
  can $readmemh('file_that_is_here.txt') but we're running out of work-dir. Linking
1159
1265
  is the easy work-around vs trying to run-in-place of all SV files.
1266
+
1267
+ Note that we also include .py and Makefile(s) in this.
1160
1268
  '''
1161
1269
 
1162
- for fname in self.files_non_source:
1270
+ for fname in self.files_non_source + self.files_py + self.files_makefile:
1163
1271
  _, leaf_fname = os.path.split(fname)
1164
1272
  destfile = os.path.join(self.args['work-dir'], leaf_fname)
1165
1273
  relfname = os.path.relpath(fname)
@@ -1518,7 +1626,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1518
1626
  # if we've found any target since being called, it means we found the one we were called for
1519
1627
  return found_target
1520
1628
 
1521
- def add_file( # pylint: disable=too-many-locals,too-many-branches
1629
+ def add_file( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
1522
1630
  self, filename: str, use_abspath: bool = True, add_to_non_sources: bool = False,
1523
1631
  caller_info: str = '', forced_extension: str = ''
1524
1632
  ) -> str:
@@ -1543,6 +1651,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1543
1651
  cpp_file_ext_list = known_file_ext_dict.get('cpp', [])
1544
1652
  sdc_file_ext_list = known_file_ext_dict.get('synth_constraints', [])
1545
1653
  dotf_file_ext_list = known_file_ext_dict.get('dotf', [])
1654
+ py_file_ext_list = known_file_ext_dict.get('python', [])
1655
+ makefile_ext_list = known_file_ext_dict.get('makefile', [])
1546
1656
 
1547
1657
  if forced_extension:
1548
1658
  # If forced_extension='systemverilog', then use the first known extension for
@@ -1582,6 +1692,12 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1582
1692
  caller_info=caller_info)
1583
1693
  dp.apply_args(args_list=[f'-f={file_abspath}'])
1584
1694
  del dp
1695
+ elif file_ext in py_file_ext_list:
1696
+ self.files_py.append(file_abspath)
1697
+ util.debug(f"Added Python file {filename} as {file_abspath}")
1698
+ elif file_ext in makefile_ext_list or os.path.split(filename)[1] == 'Makefile':
1699
+ self.files_makefile.append(file_abspath)
1700
+ util.debug(f"Added Makefile {filename} as {file_abspath}")
1585
1701
  else:
1586
1702
  # unknown file extension. In these cases we link the file to the working directory
1587
1703
  # so it is available (for example, a .mem file that is expected to exist with relative
@@ -2114,21 +2230,22 @@ class CommandParallel(Command):
2114
2230
  work_queue.put((jobs_launched, command_list, job['name'], cwd))
2115
2231
  suffix = "<START>"
2116
2232
  if fancy_mode:
2117
- util.fancy_print(job_text+suffix, worker)
2233
+ util.fancy_print(job_text + suffix, worker)
2118
2234
  elif failed_jobs:
2119
2235
  # if we aren't in fancy mode, we will print a START line, periodic RUNNING
2120
2236
  # lines, and PASS/FAIL line per-job
2121
- util.print_orange(job_text + Colors.yellow + suffix)
2237
+ util.print_yellow(job_text, end='')
2238
+ util.print_foreground_color(suffix)
2122
2239
  else:
2123
- util.print_yellow(job_text + Colors.yellow + suffix)
2240
+ util.print_foreground_color(job_text + suffix)
2124
2241
  else:
2125
2242
  # single-threaded job launch, we are going to print out job info as we start
2126
2243
  # each job... no newline. since non-verbose silences the job and prints only
2127
2244
  # <PASS>/<FAIL> after the trailing "..." we leave here
2128
2245
  if failed_jobs:
2129
- util.print_orange(job_text, end="")
2130
- else:
2131
2246
  util.print_yellow(job_text, end="")
2247
+ else:
2248
+ util.print_foreground_color(job_text, end="")
2132
2249
  job_done_number = jobs_launched
2133
2250
  job_done_name = job['name']
2134
2251
  job_start_time = time.time()
@@ -2182,9 +2299,10 @@ class CommandParallel(Command):
2182
2299
  if fancy_mode:
2183
2300
  util.fancy_print(f"{job_text}{suffix}", t['worker'])
2184
2301
  elif failed_jobs:
2185
- util.print_orange(job_text + Colors.yellow + suffix)
2302
+ util.print_yellow(job_text, end='')
2303
+ util.print_foreground_color(suffix)
2186
2304
  else:
2187
- util.print_yellow(job_text + Colors.yellow + suffix)
2305
+ util.print_foreground_color(job_text + suffix)
2188
2306
 
2189
2307
  # shared job completion code
2190
2308
  # single or multi-threaded, we can arrive here to harvest <= 1 jobs, and need
@@ -2192,36 +2310,52 @@ class CommandParallel(Command):
2192
2310
  # printed, ready for pass/fail
2193
2311
  if job_done:
2194
2312
  jobs_complete += 1
2313
+ this_job_failed = False
2195
2314
  if job_done_return_code is None or job_done_return_code:
2196
- # embed the color code, to change color of pass/fail during the
2197
- # util.print_orange/yellow below
2198
2315
  if job_done_return_code == 124:
2199
2316
  # bash uses 124 for bash timeout errors, if that was preprended to the
2200
2317
  # command list.
2201
- suffix = f"{Colors.red}<TOUT: {sprint_time(job_done_run_time)}>"
2318
+ suffix = (
2319
+ f"<TOUT{safe_emoji(' ❌')}:"
2320
+ f" {sprint_time(job_done_run_time)}>"
2321
+ )
2322
+ this_job_failed = True
2202
2323
  else:
2203
- suffix = f"{Colors.red}<FAIL: {sprint_time(job_done_run_time)}>"
2324
+ suffix = (
2325
+ f"<FAIL{safe_emoji(' ❌')}:"
2326
+ f" {sprint_time(job_done_run_time)}>"
2327
+ )
2328
+ this_job_failed = True
2204
2329
  failed_jobs.append(job_done_name)
2205
2330
  else:
2206
- suffix = f"{Colors.green}<PASS: {sprint_time(job_done_run_time)}>"
2331
+ suffix = (
2332
+ f"<PASS{safe_emoji(' ✅')}:"
2333
+ f" {sprint_time(job_done_run_time)}>"
2334
+ )
2207
2335
  passed_jobs.append(job_done_name)
2208
2336
  # we want to print in one shot, because in fancy modes that's all that we're allowed
2209
2337
  job_done_text = "" if job_done_quiet else sprint_job_line(job_done_number,
2210
2338
  job_done_name)
2211
- if failed_jobs:
2212
- util.print_orange(f"{job_done_text}{suffix}")
2213
- else:
2339
+ if this_job_failed:
2340
+ util.print_red(f"{job_done_text}{suffix}")
2341
+ elif failed_jobs:
2214
2342
  util.print_yellow(f"{job_done_text}{suffix}")
2343
+ else:
2344
+ util.print_green(f"{job_done_text}{suffix}")
2215
2345
  self.jobs_status[job_done_number-1] = job_done_return_code
2216
2346
 
2217
2347
  if not anything_done:
2218
2348
  time.sleep(0.25) # if nothing happens for an iteration, chill out a bit
2219
2349
 
2220
2350
  if total_jobs:
2221
- emoji = "< :) >" if (len(passed_jobs) == total_jobs) else "< :( >"
2222
- util.info(sprint_job_line(final=True, job_name="jobs passed") + emoji, start="")
2351
+ if len(passed_jobs) == total_jobs:
2352
+ emojitxt = safe_emoji('😀', ':)')
2353
+ else:
2354
+ emojitxt = safe_emoji('😦', ':(')
2355
+ util.info(sprint_job_line(final=True, job_name="jobs passed") + f"< {emojitxt} >",
2356
+ start="")
2223
2357
  else:
2224
- util.info("Parallel: <No jobs found>")
2358
+ util.info(f"Parallel: <{safe_emoji('❓ ')}No jobs found>")
2225
2359
  # Make sure all jobs have a set status:
2226
2360
  for i, rc in enumerate(self.jobs_status):
2227
2361
  if rc is None or not isinstance(rc, int):
opencos/eda_config.py CHANGED
@@ -15,6 +15,7 @@ import shutil
15
15
  import mergedeep
16
16
 
17
17
  from opencos import util
18
+ from opencos.util import safe_emoji
18
19
  from opencos.utils.markup_helpers import yaml_safe_load, yaml_safe_writer
19
20
 
20
21
  class Defaults:
@@ -231,7 +232,7 @@ def get_config_merged_with_defaults(config:dict) -> dict:
231
232
  def get_argparser() -> argparse.ArgumentParser:
232
233
  '''Returns an ArgumentParser, handles --config-yml=<filename> arg'''
233
234
  parser = argparse.ArgumentParser(
234
- prog='opencos eda config options', add_help=False, allow_abbrev=False
235
+ prog=f'{safe_emoji("🔎 ")}opencos eda config options', add_help=False, allow_abbrev=False
235
236
  )
236
237
  parser.add_argument('--config-yml', type=str, default=Defaults.config_yml,
237
238
  help=('YAML filename to use for configuration (default'
@@ -94,6 +94,11 @@ file_extensions:
94
94
  dotf:
95
95
  - .f
96
96
  - .vc
97
+ python:
98
+ - .py
99
+ makefile:
100
+ - .mk
101
+
97
102
 
98
103
  inferred_top:
99
104
  # file extensions that we can infer "top" module from, if --top omitted.
@@ -332,7 +337,6 @@ tools:
332
337
  - "COCOTB_TEST_FAILED"
333
338
  log-must-strings:
334
339
  - "passed"
335
- - "Cocotb test completed successfully!"
336
340
 
337
341
 
338
342
  quartus: