opencos-eda 0.3.3__py3-none-any.whl → 0.3.6__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/commands/sim.py CHANGED
@@ -145,6 +145,7 @@ class CommandSim(CommandDesign):
145
145
  self.run_dep_commands()
146
146
  self.do_it()
147
147
  self.run_post_tool_dep_commands()
148
+ self.report_pass_fail()
148
149
  return unparsed
149
150
 
150
151
 
@@ -198,6 +199,9 @@ class CommandSim(CommandDesign):
198
199
 
199
200
  clist = list(obj).copy()
200
201
  tee_fpath = getattr(obj, 'tee_fpath', None)
202
+ work_dir = getattr(obj, 'work_dir', None)
203
+ if not work_dir:
204
+ work_dir = self.args['work-dir']
201
205
 
202
206
  util.debug(f'run_commands_check_logs: {clist=}, {tee_fpath=}')
203
207
 
@@ -207,24 +211,28 @@ class CommandSim(CommandDesign):
207
211
  if log_filename:
208
212
  log_fname = log_filename
209
213
 
214
+
210
215
  _, stdout, _ = self.exec(
211
- work_dir=self.args['work-dir'], command_list=clist, tee_fpath=tee_fpath
216
+ work_dir=work_dir, command_list=clist, tee_fpath=tee_fpath
212
217
  )
213
218
 
214
219
  if check_logs and log_fname:
215
220
  # Note this call will check on stdout if not GUI, not opening the log_fname,
216
221
  # but if this is GUI we normally lose stdout and have to open the log.
217
- gui_mode = self.args.get('gui', False)
218
- file_contents_str = '' if gui_mode else stdout
222
+ if self.args.get('gui', False):
223
+ file_contents_str = ''
224
+ else:
225
+ file_contents_str = stdout
226
+
219
227
  self.check_logs_for_errors(
220
- filename=os.path.join(self.args['work-dir'], log_fname),
228
+ filename=os.path.join(work_dir, log_fname),
221
229
  file_contents_str=file_contents_str,
222
230
  bad_strings=bad_strings, must_strings=must_strings,
223
231
  use_bad_strings=use_bad_strings, use_must_strings=use_must_strings
224
232
  )
225
233
  if log_fname:
226
234
  self.artifacts_add(
227
- name=os.path.join(self.args['work-dir'], log_fname),
235
+ name=os.path.join(work_dir, log_fname),
228
236
  typ='text', description='Simulator stdout/stderr log file'
229
237
  )
230
238
 
@@ -253,6 +261,7 @@ class CommandSim(CommandDesign):
253
261
  tool = self.args.get('tool', None)
254
262
  # Certain args are allow-listed here
255
263
  deps_file_args = []
264
+ print(f'SUPER DREW DEBUG: {self.get_command_line_args()=}')
256
265
  for a in self.get_command_line_args():
257
266
  if any(a.startswith(x) for x in [
258
267
  '--compile-args',
@@ -265,7 +274,8 @@ class CommandSim(CommandDesign):
265
274
  '--stop-',
266
275
  '--lint-',
267
276
  '--verilate',
268
- '--verilator']):
277
+ '--verilator',
278
+ '--cocotb-test-']):
269
279
  deps_file_args.append(a)
270
280
 
271
281
  export_obj.run(
opencos/deps/defaults.py CHANGED
@@ -33,6 +33,7 @@ SUPPORTED_TARGET_TABLE_KEYS = set([
33
33
  'defines',
34
34
  'parameters',
35
35
  'incdirs',
36
+ 'plusargs',
36
37
  'top',
37
38
  'deps',
38
39
  'reqs',
@@ -10,7 +10,7 @@ import os
10
10
  from opencos import files
11
11
  from opencos import eda_config
12
12
  from opencos.util import debug, info, warning, error, read_tokens_from_dot_f, \
13
- patch_args_for_dir
13
+ patch_args_for_dir, load_env_file
14
14
  from opencos.utils.str_helpers import dep_str2list
15
15
  from opencos.deps.deps_file import deps_target_get_deps_list
16
16
  from opencos.deps.deps_commands import deps_commands_handler
@@ -77,7 +77,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
77
77
  ).get('command_handler', {}).keys()
78
78
 
79
79
 
80
- def apply_defines(self, defines_dict: dict):
80
+ def apply_defines(self, defines_dict: dict) -> None:
81
81
  '''Given defines_dict, applies them to our self.command_design_ref obj'''
82
82
  if not isinstance(defines_dict, dict):
83
83
  self.error(f"{defines_dict=} is not type dict, can't apply defines,",
@@ -95,7 +95,19 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
95
95
  self.command_design_ref.process_plusarg(f'+define+{k}={v}')
96
96
 
97
97
 
98
- def apply_parameters(self, parameters_dict: dict):
98
+ def apply_plusargs(self, plusargs_dict: dict) -> None:
99
+ '''Given plusarsg_dict, applies them to our self.command_design_ref obj'''
100
+ if not isinstance(plusargs_dict, dict):
101
+ self.error(f"{plusargs_dict=} is not type dict, can't apply plusargs,",
102
+ f"in {self.caller_info}")
103
+ for k,v in plusargs_dict.items():
104
+ if v is None or v == '':
105
+ self.command_design_ref.process_plusarg(f'+{k}')
106
+ else:
107
+ self.command_design_ref.process_plusarg(f'+{k}={v}')
108
+
109
+
110
+ def apply_parameters(self, parameters_dict: dict) -> None:
99
111
  '''Given parameters_dict, applies them to our self.command_design_ref obj'''
100
112
  if not isinstance(parameters_dict, dict):
101
113
  self.error(f"{parameters_dict=} is not type dict, can't apply defines,",
@@ -110,7 +122,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
110
122
  )
111
123
 
112
124
 
113
- def apply_incdirs(self, incdirs_list:list):
125
+ def apply_incdirs(self, incdirs_list:list) -> None:
114
126
  '''Given incdirs_list, applies them to our self.command_design_ref obj'''
115
127
  if not isinstance(incdirs_list, (str, list)):
116
128
  self.error(f"{incdirs_list=} is not type str/list, can't apply incdirs",
@@ -180,9 +192,15 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
180
192
  # Since some args (util.py, eda_config.py, eda.py) can only be handled from command
181
193
  # line, it would be nice if -f or --input-file is handled from DEPS, so we'll special
182
194
  # case that now. Recursively resolve -f / --input-file.
195
+ # Do similary for --env-file (also only supported in util.py)
183
196
  parser = argparse.ArgumentParser(
184
197
  prog='deps_processor -f/--input-file', add_help=False, allow_abbrev=False
185
198
  )
199
+ parser.add_argument('--env-file', default=[], action='append',
200
+ help=(
201
+ "dotenv file(s) to pass ENV vars, (default: .env loaded first,"
202
+ " subsequent files' vars override .env"
203
+ ))
186
204
  parser.add_argument('-f', '--input-file', default=[], action='append',
187
205
  help=(
188
206
  'Input .f file to be expanded as eda args, defines, incdirs,'
@@ -213,7 +231,12 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
213
231
  # recurse until we've resolved nested .f files.
214
232
  return self.apply_args(args_list=tokens2)
215
233
 
216
- tokens = tokens2 # if no --input-file values, keep parsing the remaining tokens2
234
+ if parsed.env_file:
235
+ for env_file in parsed.env_file:
236
+ load_env_file(env_file)
237
+
238
+ # if no --input-file/--env-file values, keep parsing the remaining tokens2:
239
+ tokens = tokens2
217
240
 
218
241
  # We have to special-case anything with --tool[=value] in tokens, otherwise
219
242
  # the user may think they were allowed to set --tool, but in our flow the Command handler
@@ -290,7 +313,9 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
290
313
  caller_info=self.caller_info
291
314
  )
292
315
 
293
- def process_deps_entry(self):
316
+ def process_deps_entry( # pylint: disable=too-many-branches
317
+ self
318
+ ) -> list:
294
319
  '''Main entry point (after creating DepsProcessor obj) to resolve a deps target
295
320
 
296
321
  Example usage:
@@ -303,22 +328,6 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
303
328
  This method will apply all target features to the CommandDesign ref object as
304
329
  we traverse.
305
330
 
306
- Supported target keys:
307
- -- tags (or equivalent, to support multiple define/incdir/deps for a target)
308
- -- supports tag-name, with-tools, with-args, args, defines, incdirs, deps
309
- ** to be applied if a tool matches.
310
- -- TODO(drew): other features in docs/DEPS.md not yet implemented.
311
- -- multi: ignore-this-target: - commands (handled in eda.py CommandMulti.resolve_target)
312
- -- Named eda commands
313
- -- (partially done) sim or other eda commands (eda.py command specific things)
314
- basically, check the command, and apply/merge values to 'entry'?
315
- -- args
316
- -- defines
317
- -- incdirs
318
- -- top.
319
- -- commands (not in deps)
320
- -- deps
321
-
322
331
  TODO(drew): This does not yet support conditional inclusions based on defines,
323
332
  like the old DEPS files did with pattern:
324
333
  SOME_DEFINE ? dep_if_define_present : dep_if_define_not_present
@@ -344,6 +353,8 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
344
353
  remaining_deps_list += self.process_tags()
345
354
  elif key == 'defines':
346
355
  self.process_defines()
356
+ elif key == 'plusargs':
357
+ self.process_plusargs()
347
358
  elif key == 'parameters':
348
359
  self.process_parameters()
349
360
  elif key == 'incdirs':
@@ -520,6 +531,10 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
520
531
  # apply defines:
521
532
  self.apply_defines(value.get('defines', {}))
522
533
 
534
+ if key == 'plusargs':
535
+ # apply plusargs:
536
+ self.apply_plusargs(value.get('plusargs', {}))
537
+
523
538
  elif key == 'parameters':
524
539
  self.apply_parameters(value.get('parameters', {}))
525
540
 
@@ -598,7 +613,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
598
613
  return ret_deps_added_from_tags
599
614
 
600
615
 
601
- def process_defines(self):
616
+ def process_defines(self) -> None:
602
617
  '''Returns None, applies defines (dict, if any) from self.deps_entry to
603
618
  self.command_design_ref.'''
604
619
 
@@ -614,7 +629,28 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
614
629
 
615
630
  self.apply_defines(entry_defines)
616
631
 
617
- def process_parameters(self):
632
+
633
+ def process_plusargs(self) -> None:
634
+ '''Returns None, applies plusargs (dict, if any) from self.deps_entry to
635
+ self.command_design_ref.
636
+
637
+ These work w/ the same rules as defines (no value, or value int/str)
638
+ '''
639
+
640
+ # Plusargs:
641
+ # apply command specific plusargs, with higher priority than the a
642
+ # deps_entry['sim']['plusargs'] entry,
643
+ # do this with dict1.update(dict2):
644
+ entry_plusargs = {}
645
+ entry_plusargs.update(self.deps_entry.get('plusargs', {}))
646
+ entry_plusargs.update(self.entry_eda_command.get('plusargs', {}))
647
+ assert isinstance(entry_plusargs, dict), \
648
+ f'{entry_plusargs=} for in {self.caller_info} must be a dict'
649
+
650
+ self.apply_plusargs(entry_plusargs)
651
+
652
+
653
+ def process_parameters(self) -> None:
618
654
  '''Returns None, applies parameters (dict, if any) from self.deps_entry to
619
655
  self.command_design_ref.'''
620
656
 
opencos/deps_schema.py CHANGED
@@ -17,6 +17,9 @@ my_target_name:
17
17
  incdirs: <---- incdirs, optional array (or string)
18
18
  - ./
19
19
 
20
+ plusargs: <---- plusargs, optional table
21
+ some_plusarg: 32
22
+
20
23
  top: tb <---- top, optional string
21
24
 
22
25
  deps:
@@ -63,6 +66,7 @@ my_target_name:
63
66
  defines: <---- defines, optional table
64
67
  parameters: <---- parameters, optional table
65
68
  incdirs: <---- incdirs, optional array (or string)
69
+ plusargs: <---- plusargs, optional table
66
70
  top: tb <---- top, optional string
67
71
  deps: <---- TARGET_DEPS_CONTENTS schema
68
72
  - some_file.sv <---- string file name
@@ -216,6 +220,9 @@ TARGET_EDA_COMMAND_ENTRY_TABLE = {
216
220
  Optional('defines'): {
217
221
  Optional(str): Or(str, int, type(None)),
218
222
  },
223
+ Optional('plusargs'): {
224
+ Optional(str): Or(str, int, type(None)),
225
+ },
219
226
  Optional('parameters'): {
220
227
  Optional(str): Or(str, int),
221
228
  },
@@ -241,6 +248,9 @@ TARGET_TAGS_TABLE = {
241
248
  Optional('defines'): {
242
249
  Optional(str): Or(str, int, type(None)),
243
250
  },
251
+ Optional('plusargs'): {
252
+ Optional(str): Or(str, int, type(None)),
253
+ },
244
254
  Optional('parameters'): {
245
255
  Optional(str): Or(str, int),
246
256
  },
@@ -262,6 +272,10 @@ TARGET_CONTENTS = Or(
262
272
  Optional('defines'): {
263
273
  Optional(str): Or(str, int, type(None)),
264
274
  },
275
+ # plusargs: table of key-value; value null or string
276
+ Optional('plusargs'): {
277
+ Optional(str): Or(str, int, type(None)),
278
+ },
265
279
  # parameters: table of key-value
266
280
  Optional('parameters'): {
267
281
  Optional(str): Or(str, int),
@@ -324,6 +338,9 @@ FILE_SIMPLIFIED = Schema(
324
338
  Optional('defines'): {
325
339
  Optional(str): Or(type(None), str),
326
340
  },
341
+ Optional('plusargs'): {
342
+ Optional(str): Or(type(None), str),
343
+ },
327
344
  Optional('parameters'): {
328
345
  Optional(str): str,
329
346
  },
opencos/eda.py CHANGED
@@ -20,10 +20,11 @@ from pathlib import Path
20
20
 
21
21
  import opencos
22
22
  from opencos import util, eda_config, eda_base
23
+ from opencos.util import safe_emoji
23
24
  from opencos.eda_base import Tool, which_tool, get_eda_exec
24
25
  from opencos.utils import vsim_helper, vscode_helper
25
26
  from opencos.utils.subprocess_helpers import subprocess_run_background
26
- from opencos.utils import status_constants, str_helpers
27
+ from opencos.utils import status_constants, str_helpers, subprocess_helpers
27
28
 
28
29
  # Configure util:
29
30
  util.progname = "EDA"
@@ -50,7 +51,7 @@ def init_config(
50
51
  # For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
51
52
  # the actual class using importlib (via opencos.util)
52
53
 
53
- eda_config.tool_try_add_to_path(tool)
54
+ eda_config.update_config_auto_tool_order_for_tool(tool=tool, config=config)
54
55
 
55
56
  config['command_handler'] = {}
56
57
  for _cmd, str_class in config['DEFAULT_HANDLERS'].items():
@@ -95,15 +96,15 @@ def usage(tokens: list, config: dict, command: str = "", tool: str = "") -> int:
95
96
 
96
97
  if command == "":
97
98
  print(
98
- """
99
- Usage:
99
+ f"""
100
+ {safe_emoji("🔦 ")}Usage:
100
101
  eda [<options>] <command> [options] <files|targets, ...>
101
102
  """
102
103
  )
103
104
  print(get_all_commands_help_str(config=config))
104
105
  print(
105
- """
106
- And <files|targets, ...> is one or more source file or DEPS markup file target,
106
+ f"""
107
+ {safe_emoji("❕ ")}where <files|targets, ...> is one or more source file or DEPS markup file target,
107
108
  such as .v, .sv, .vhd[l], .cpp files, or a target key in a DEPS.[yml|yaml|toml|json].
108
109
  Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` to
109
110
  force use that file as systemverilog, verilog, vhdl, or C++, respectively.
@@ -163,10 +164,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
163
164
  If so, updates config['auto_tools_order'][tool]['exe']
164
165
  '''
165
166
 
166
-
167
- tool = eda_config.update_config_auto_tool_order_for_tool(
168
- tool=tool, config=config
169
- )
167
+ tool = eda_config.tool_arg_remove_path_information(tool)
170
168
 
171
169
  assert 'auto_tools_order' in config
172
170
  assert isinstance(config['auto_tools_order'], list)
@@ -275,9 +273,7 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
275
273
 
276
274
  '''
277
275
 
278
- tool = eda_config.update_config_auto_tool_order_for_tool(
279
- tool=tool, config=config
280
- )
276
+ tool = eda_config.tool_arg_remove_path_information(tool)
281
277
 
282
278
  if not quiet and not auto_setup:
283
279
  util.info(f"Setup for tool: '{tool}'")
@@ -389,6 +385,8 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
389
385
  if not is_interactive:
390
386
  # Run init_config() now, we deferred it in main(), but only run it
391
387
  # for this tool (or tool=None to figure it out)
388
+ # This will handle any --tool=<name>=/path/to/bin also, so don't have to
389
+ # run tool_setup(..) on its own.
392
390
  config = init_config(
393
391
  config, tool=parsed.tool,
394
392
  run_auto_tool_setup=run_auto_tool_setup
@@ -406,13 +404,8 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
406
404
  for arg in unparsed:
407
405
  if not arg.startswith('-'):
408
406
  command = arg
409
- if parsed.tool:
410
- tool_setup(parsed.tool, config=config)
411
407
  return usage(tokens=unparsed, config=config, command=command, tool=parsed.tool)
412
408
 
413
- if parsed.tool:
414
- tool_setup(parsed.tool, config=config)
415
-
416
409
  deferred_tokens = unparsed
417
410
  if not command:
418
411
  util.error("Didn't get a command!")
@@ -503,7 +496,8 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
503
496
  def signal_handler(sig, frame) -> None: # pylint: disable=unused-argument
504
497
  '''Handles Ctrl-C, called by main_cli() if running from command line'''
505
498
  util.fancy_stop()
506
- util.info('Received Ctrl+C...', start='\nINFO: [EDA] ')
499
+ util.error(f'{safe_emoji("❌ ")}Received Ctrl+C...', start='\nINFO: [EDA] ')
500
+ subprocess_helpers.cleanup_all()
507
501
  util.exit(-1)
508
502
 
509
503
  # **************************************************************
opencos/eda_base.py CHANGED
@@ -24,7 +24,7 @@ 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
29
  indent_wrap_long_text, pretty_list_columns_manual
30
30
  from opencos.utils.subprocess_helpers import subprocess_run_background
@@ -44,7 +44,9 @@ def print_base_help() -> None:
44
44
 
45
45
  def get_argparser() -> argparse.ArgumentParser:
46
46
  '''Returns the ArgumentParser for general eda CLI'''
47
- parser = argparse.ArgumentParser(prog='eda options', add_help=False, allow_abbrev=False)
47
+ parser = argparse.ArgumentParser(
48
+ prog=f'{safe_emoji("🔎 ")}eda options', add_help=False, allow_abbrev=False
49
+ )
48
50
  parser.add_argument('-q', '--quit', action='store_true',
49
51
  help=(
50
52
  'For interactive mode (eda called with no options, command, or'
@@ -313,6 +315,18 @@ class Command: # pylint: disable=too-many-public-methods
313
315
  util.error(f"command '{self.command_name}' has previous errors")
314
316
  return self.status > 0
315
317
 
318
+ def report_pass_fail(self) -> None:
319
+ '''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
+
329
+
316
330
  def which_tool(self, command:str) -> str:
317
331
  '''Returns a str for the tool name used for the requested command'''
318
332
  return which_tool(command, config=self.config)
@@ -462,7 +476,8 @@ class Command: # pylint: disable=too-many-public-methods
462
476
  if not tee_fpath and getattr(command_list, 'tee_fpath', None):
463
477
  tee_fpath = getattr(command_list, 'tee_fpath', '')
464
478
  if not quiet:
465
- util.info(f"exec: {' '.join(command_list)} (in {work_dir}, {tee_fpath=})")
479
+ util.info(f"{safe_emoji('⏩ ')}exec: {' '.join(command_list)}",
480
+ f"(in {work_dir}, {tee_fpath=})")
466
481
 
467
482
  stdout, stderr, return_code = subprocess_run_background(
468
483
  work_dir=work_dir,
@@ -484,7 +499,8 @@ class Command: # pylint: disable=too-many-public-methods
484
499
  self.error(f"exec: returned with error (return code: {return_code})",
485
500
  error_code=self.status)
486
501
  else:
487
- util.debug(f"exec: returned with error (return code: {return_code})")
502
+ util.debug(f"{safe_emoji('❌ ')}exec: returned with error (return code:",
503
+ f"{return_code})")
488
504
  else:
489
505
  util.debug(f"exec: returned without error (return code: {return_code})")
490
506
  return stderr, stdout, return_code
@@ -564,7 +580,9 @@ class Command: # pylint: disable=too-many-public-methods
564
580
  # parsed.args-with-dashes is not legal python. Some of self.args.keys() still have - or _,
565
581
  # so this will handle both.
566
582
  # Also, preference is for self.args.keys(), to be str with - dashes
567
- parser = argparse.ArgumentParser(prog='eda', add_help=False, allow_abbrev=False)
583
+ parser = argparse.ArgumentParser(
584
+ prog=f'{safe_emoji("🔎 ")}eda', add_help=False, allow_abbrev=False
585
+ )
568
586
  bool_action_kwargs = util.get_argparse_bool_action_kwargs()
569
587
 
570
588
  if not parser_arg_list:
@@ -594,6 +612,7 @@ class Command: # pylint: disable=too-many-public-methods
594
612
  help_kwargs = {'help': f'default={value}'}
595
613
  else:
596
614
  help_kwargs = {'help': f'{type(value).__name__} default={value}'}
615
+ help_kwargs['help'] = help_kwargs['help'].replace('%', '%%')
597
616
 
598
617
 
599
618
  # It's important to set the default=None on these, except for list types where default
@@ -796,7 +815,7 @@ class Command: # pylint: disable=too-many-public-methods
796
815
  util.info('Help:')
797
816
  # using bare 'print' here, since help was requested, avoids --color and --quiet
798
817
  print()
799
- print('Usage:')
818
+ print(f'{safe_emoji("🔦 ")}Usage:')
800
819
  if no_targets:
801
820
  print(f' eda [options] {self.command_name} [options]')
802
821
  else:
@@ -810,10 +829,10 @@ class Command: # pylint: disable=too-many-public-methods
810
829
  return
811
830
 
812
831
  if self.command_name:
813
- lines.append(f"Generic help for command='{self.command_name}'"
832
+ lines.append(f"{safe_emoji('🔧 ')}Generic help for command='{self.command_name}'"
814
833
  f" (using '{self.__class__.__name__}')")
815
834
  else:
816
- lines.append("Generic help (from class Command):")
835
+ lines.append("{safe_emoji('🔧 ')}Generic help (from class Command):")
817
836
 
818
837
  # Attempt to run argparser on args, but don't error if it fails.
819
838
  unparsed = []
@@ -979,6 +998,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
979
998
  self.files_vhd = []
980
999
  self.files_cpp = []
981
1000
  self.files_sdc = []
1001
+ self.files_py = []
1002
+ self.files_makefile = []
982
1003
  self.files_non_source = []
983
1004
  self.files_caller_info = {}
984
1005
  self.dep_shell_commands = [] # each list entry is a {}
@@ -1138,7 +1159,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1138
1159
  self.files[new_key] = True
1139
1160
 
1140
1161
  my_file_lists_list = [self.files_v, self.files_sv, self.files_vhd, self.files_cpp,
1141
- self.files_sdc]
1162
+ self.files_sdc, self.files_py, self.files_makefile]
1142
1163
  for my_file_list in my_file_lists_list:
1143
1164
  for i,value in enumerate(my_file_list):
1144
1165
  if value and isinstance(value, str) and \
@@ -1156,9 +1177,11 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1156
1177
  need to be copied or linked to the work-dir. For example, if some SV assumes it
1157
1178
  can $readmemh('file_that_is_here.txt') but we're running out of work-dir. Linking
1158
1179
  is the easy work-around vs trying to run-in-place of all SV files.
1180
+
1181
+ Note that we also include .py and Makefile(s) in this.
1159
1182
  '''
1160
1183
 
1161
- for fname in self.files_non_source:
1184
+ for fname in self.files_non_source + self.files_py + self.files_makefile:
1162
1185
  _, leaf_fname = os.path.split(fname)
1163
1186
  destfile = os.path.join(self.args['work-dir'], leaf_fname)
1164
1187
  relfname = os.path.relpath(fname)
@@ -1517,7 +1540,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1517
1540
  # if we've found any target since being called, it means we found the one we were called for
1518
1541
  return found_target
1519
1542
 
1520
- def add_file( # pylint: disable=too-many-locals,too-many-branches
1543
+ def add_file( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
1521
1544
  self, filename: str, use_abspath: bool = True, add_to_non_sources: bool = False,
1522
1545
  caller_info: str = '', forced_extension: str = ''
1523
1546
  ) -> str:
@@ -1542,6 +1565,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1542
1565
  cpp_file_ext_list = known_file_ext_dict.get('cpp', [])
1543
1566
  sdc_file_ext_list = known_file_ext_dict.get('synth_constraints', [])
1544
1567
  dotf_file_ext_list = known_file_ext_dict.get('dotf', [])
1568
+ py_file_ext_list = known_file_ext_dict.get('python', [])
1569
+ makefile_ext_list = known_file_ext_dict.get('makefile', [])
1545
1570
 
1546
1571
  if forced_extension:
1547
1572
  # If forced_extension='systemverilog', then use the first known extension for
@@ -1581,6 +1606,12 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1581
1606
  caller_info=caller_info)
1582
1607
  dp.apply_args(args_list=[f'-f={file_abspath}'])
1583
1608
  del dp
1609
+ elif file_ext in py_file_ext_list:
1610
+ self.files_py.append(file_abspath)
1611
+ util.debug(f"Added Python file {filename} as {file_abspath}")
1612
+ elif file_ext in makefile_ext_list or os.path.split(filename)[1] == 'Makefile':
1613
+ self.files_makefile.append(file_abspath)
1614
+ util.debug(f"Added Makefile {filename} as {file_abspath}")
1584
1615
  else:
1585
1616
  # unknown file extension. In these cases we link the file to the working directory
1586
1617
  # so it is available (for example, a .mem file that is expected to exist with relative
@@ -2113,21 +2144,22 @@ class CommandParallel(Command):
2113
2144
  work_queue.put((jobs_launched, command_list, job['name'], cwd))
2114
2145
  suffix = "<START>"
2115
2146
  if fancy_mode:
2116
- util.fancy_print(job_text+suffix, worker)
2147
+ util.fancy_print(job_text + suffix, worker)
2117
2148
  elif failed_jobs:
2118
2149
  # if we aren't in fancy mode, we will print a START line, periodic RUNNING
2119
2150
  # lines, and PASS/FAIL line per-job
2120
- util.print_orange(job_text + Colors.yellow + suffix)
2151
+ util.print_yellow(job_text, end='')
2152
+ util.print_foreground_color(suffix)
2121
2153
  else:
2122
- util.print_yellow(job_text + Colors.yellow + suffix)
2154
+ util.print_foreground_color(job_text + suffix)
2123
2155
  else:
2124
2156
  # single-threaded job launch, we are going to print out job info as we start
2125
2157
  # each job... no newline. since non-verbose silences the job and prints only
2126
2158
  # <PASS>/<FAIL> after the trailing "..." we leave here
2127
2159
  if failed_jobs:
2128
- util.print_orange(job_text, end="")
2129
- else:
2130
2160
  util.print_yellow(job_text, end="")
2161
+ else:
2162
+ util.print_foreground_color(job_text, end="")
2131
2163
  job_done_number = jobs_launched
2132
2164
  job_done_name = job['name']
2133
2165
  job_start_time = time.time()
@@ -2181,9 +2213,10 @@ class CommandParallel(Command):
2181
2213
  if fancy_mode:
2182
2214
  util.fancy_print(f"{job_text}{suffix}", t['worker'])
2183
2215
  elif failed_jobs:
2184
- util.print_orange(job_text + Colors.yellow + suffix)
2216
+ util.print_yellow(job_text, end='')
2217
+ util.print_foreground_color(suffix)
2185
2218
  else:
2186
- util.print_yellow(job_text + Colors.yellow + suffix)
2219
+ util.print_foreground_color(job_text + suffix)
2187
2220
 
2188
2221
  # shared job completion code
2189
2222
  # single or multi-threaded, we can arrive here to harvest <= 1 jobs, and need
@@ -2191,36 +2224,52 @@ class CommandParallel(Command):
2191
2224
  # printed, ready for pass/fail
2192
2225
  if job_done:
2193
2226
  jobs_complete += 1
2227
+ this_job_failed = False
2194
2228
  if job_done_return_code is None or job_done_return_code:
2195
- # embed the color code, to change color of pass/fail during the
2196
- # util.print_orange/yellow below
2197
2229
  if job_done_return_code == 124:
2198
2230
  # bash uses 124 for bash timeout errors, if that was preprended to the
2199
2231
  # command list.
2200
- suffix = f"{Colors.red}<TOUT: {sprint_time(job_done_run_time)}>"
2232
+ suffix = (
2233
+ f"<TOUT{safe_emoji(' ❌')}:"
2234
+ f" {sprint_time(job_done_run_time)}>"
2235
+ )
2236
+ this_job_failed = True
2201
2237
  else:
2202
- suffix = f"{Colors.red}<FAIL: {sprint_time(job_done_run_time)}>"
2238
+ suffix = (
2239
+ f"<FAIL{safe_emoji(' ❌')}:"
2240
+ f" {sprint_time(job_done_run_time)}>"
2241
+ )
2242
+ this_job_failed = True
2203
2243
  failed_jobs.append(job_done_name)
2204
2244
  else:
2205
- suffix = f"{Colors.green}<PASS: {sprint_time(job_done_run_time)}>"
2245
+ suffix = (
2246
+ f"<PASS{safe_emoji(' ✅')}:"
2247
+ f" {sprint_time(job_done_run_time)}>"
2248
+ )
2206
2249
  passed_jobs.append(job_done_name)
2207
2250
  # we want to print in one shot, because in fancy modes that's all that we're allowed
2208
2251
  job_done_text = "" if job_done_quiet else sprint_job_line(job_done_number,
2209
2252
  job_done_name)
2210
- if failed_jobs:
2211
- util.print_orange(f"{job_done_text}{suffix}")
2212
- else:
2253
+ if this_job_failed:
2254
+ util.print_red(f"{job_done_text}{suffix}")
2255
+ elif failed_jobs:
2213
2256
  util.print_yellow(f"{job_done_text}{suffix}")
2257
+ else:
2258
+ util.print_green(f"{job_done_text}{suffix}")
2214
2259
  self.jobs_status[job_done_number-1] = job_done_return_code
2215
2260
 
2216
2261
  if not anything_done:
2217
2262
  time.sleep(0.25) # if nothing happens for an iteration, chill out a bit
2218
2263
 
2219
2264
  if total_jobs:
2220
- emoji = "< :) >" if (len(passed_jobs) == total_jobs) else "< :( >"
2221
- util.info(sprint_job_line(final=True, job_name="jobs passed") + emoji, start="")
2265
+ if len(passed_jobs) == total_jobs:
2266
+ emojitxt = safe_emoji('😀', ':)')
2267
+ else:
2268
+ emojitxt = safe_emoji('😦', ':(')
2269
+ util.info(sprint_job_line(final=True, job_name="jobs passed") + f"< {emojitxt} >",
2270
+ start="")
2222
2271
  else:
2223
- util.info("Parallel: <No jobs found>")
2272
+ util.info(f"Parallel: <{safe_emoji('❓ ')}No jobs found>")
2224
2273
  # Make sure all jobs have a set status:
2225
2274
  for i, rc in enumerate(self.jobs_status):
2226
2275
  if rc is None or not isinstance(rc, int):