opencos-eda 0.2.57__py3-none-any.whl → 0.3.1__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.
Files changed (36) hide show
  1. opencos/_version.py +6 -3
  2. opencos/_waves_pkg.sv +34 -2
  3. opencos/commands/build.py +1 -0
  4. opencos/commands/export.py +1 -0
  5. opencos/commands/flist.py +1 -0
  6. opencos/commands/lec.py +1 -0
  7. opencos/commands/proj.py +1 -0
  8. opencos/commands/shell.py +4 -0
  9. opencos/commands/sim.py +47 -1
  10. opencos/commands/synth.py +4 -0
  11. opencos/deps/defaults.py +15 -7
  12. opencos/deps/deps_commands.py +84 -74
  13. opencos/deps/deps_file.py +11 -5
  14. opencos/deps/deps_processor.py +79 -3
  15. opencos/deps_schema.py +3 -0
  16. opencos/eda.py +2 -1
  17. opencos/eda_base.py +105 -34
  18. opencos/eda_config_defaults.yml +5 -1
  19. opencos/files.py +1 -0
  20. opencos/tests/deps_files/command_order/DEPS.yml +11 -0
  21. opencos/tests/helpers.py +50 -20
  22. opencos/tests/test_deps_helpers.py +37 -25
  23. opencos/tests/test_eda.py +26 -60
  24. opencos/tools/modelsim_ase.py +17 -9
  25. opencos/tools/riviera.py +31 -6
  26. opencos/tools/verilator.py +28 -38
  27. opencos/util.py +111 -16
  28. opencos/utils/vscode_helper.py +1 -1
  29. opencos/utils/vsim_helper.py +1 -1
  30. {opencos_eda-0.2.57.dist-info → opencos_eda-0.3.1.dist-info}/METADATA +2 -1
  31. {opencos_eda-0.2.57.dist-info → opencos_eda-0.3.1.dist-info}/RECORD +36 -36
  32. {opencos_eda-0.2.57.dist-info → opencos_eda-0.3.1.dist-info}/WHEEL +0 -0
  33. {opencos_eda-0.2.57.dist-info → opencos_eda-0.3.1.dist-info}/entry_points.txt +0 -0
  34. {opencos_eda-0.2.57.dist-info → opencos_eda-0.3.1.dist-info}/licenses/LICENSE +0 -0
  35. {opencos_eda-0.2.57.dist-info → opencos_eda-0.3.1.dist-info}/licenses/LICENSE.spdx +0 -0
  36. {opencos_eda-0.2.57.dist-info → opencos_eda-0.3.1.dist-info}/top_level.txt +0 -0
@@ -4,11 +4,13 @@ a DEPS markup files targets (applying deps, reqs, commands, tags, incdirs, defin
4
4
  CommandDesign ref object
5
5
  '''
6
6
 
7
+ import argparse
7
8
  import os
8
9
 
9
10
  from opencos import files
10
11
  from opencos import eda_config
11
- from opencos.util import debug, info, warning, error
12
+ from opencos.util import debug, info, warning, error, read_tokens_from_dot_f, \
13
+ patch_args_for_dir
12
14
  from opencos.utils.str_helpers import dep_str2list
13
15
  from opencos.deps.deps_file import deps_target_get_deps_list
14
16
  from opencos.deps.deps_commands import deps_commands_handler
@@ -48,6 +50,7 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
48
50
  self.target_path = target_path
49
51
  self.target_node = target_node # for debug
50
52
  self.deps_file = deps_file # for debug
53
+ self.deps_dir, _ = os.path.split(deps_file)
51
54
  self.caller_info = caller_info
52
55
 
53
56
  assert isinstance(deps_entry, dict), \
@@ -150,7 +153,9 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
150
153
  return tokens
151
154
 
152
155
 
153
- def apply_args(self, args_list:list) -> list:
156
+ def apply_args( # pylint: disable=too-many-locals,too-many-branches
157
+ self, args_list:list
158
+ ) -> list:
154
159
  '''Given args_list, applies them to our self.command_design_ref obj
155
160
 
156
161
  This will return unparsed args that weren't in the self.command_design_ref.args keys
@@ -162,10 +167,54 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
162
167
  f"in {self.caller_info}")
163
168
  tokens = dep_str2list(args_list)
164
169
 
170
+ # patch args relative to the DEPS (if self.deps_dir exists) so things like
171
+ # --build-tcl=<file> for relative <file> works when calling targets from any directory.
172
+ tokens = patch_args_for_dir(
173
+ tokens=tokens, patch_dir=self.deps_dir, caller_info=self.caller_info
174
+ )
175
+
165
176
  # We're going to run an ArgumentParser here, which is not the most efficient
166
177
  # thing to do b/c it runs on all of self.command_design_ref.args (dict) even
167
178
  # if we're applying a single token.
168
179
 
180
+ # Since some args (util.py, eda_config.py, eda.py) can only be handled from command
181
+ # line, it would be nice if -f or --input-file is handled from DEPS, so we'll special
182
+ # case that now. Recursively resolve -f / --input-file.
183
+ parser = argparse.ArgumentParser(
184
+ prog='deps_processor -f/--input-file', add_help=False, allow_abbrev=False
185
+ )
186
+ parser.add_argument('-f', '--input-file', default=[], action='append',
187
+ help=(
188
+ 'Input .f file to be expanded as eda args, defines, incdirs,'
189
+ ' files, or targets.'
190
+ ))
191
+ try:
192
+ parsed, unparsed = parser.parse_known_args(tokens + [''])
193
+ tokens2 = list(filter(None, unparsed))
194
+ except argparse.ArgumentError:
195
+ error('deps_processor -f/--input-file, problem attempting to parse_known_args for:',
196
+ f'{tokens}')
197
+ tokens2 = tokens
198
+
199
+ if parsed.input_file:
200
+ dotf_tokens = []
201
+ for filepath in parsed.input_file:
202
+ # Since this isn't command line, we have to assume the path is relative
203
+ # to this DEPS file.
204
+ if not os.path.isabs(filepath):
205
+ filepath = os.path.join(self.deps_dir, filepath)
206
+ dotf_tokens.extend(read_tokens_from_dot_f(
207
+ filepath=filepath, caller_info=self.caller_info, verbose=True
208
+ ))
209
+
210
+ # put the .f files before the unparsed args.
211
+ tokens2 = dotf_tokens + tokens2
212
+
213
+ # recurse until we've resolved nested .f files.
214
+ return self.apply_args(args_list=tokens2)
215
+
216
+ tokens = tokens2 # if no --input-file values, keep parsing the remaining tokens2
217
+
169
218
  # We have to special-case anything with --tool[=value] in tokens, otherwise
170
219
  # the user may think they were allowed to set --tool, but in our flow the Command handler
171
220
  # (self.command_design_ref) has already been chosen, so setting the tool can have
@@ -180,11 +229,38 @@ class DepsProcessor: # pylint: disable=too-many-instance-attributes
180
229
  _, unparsed = self.command_design_ref.run_argparser_on_list(
181
230
  tokens=tokens
182
231
  )
232
+
183
233
  # Annoying, but check for plusargs in unparsed, and have referenced CommandDesign
184
234
  # or CommandSim class handle it with process_plusarg.
185
- for arg in unparsed:
235
+ for arg in list(unparsed):
186
236
  if arg.startswith('+'):
187
237
  self.command_design_ref.process_plusarg(plusarg=arg, pwd=self.target_path)
238
+ unparsed.remove(arg)
239
+
240
+ # For any leftover files, or targets, attempt to process those too:
241
+ for arg in list(unparsed):
242
+ # Since this isn't command line, we have to assume for files, the path is relative
243
+ # to this DEPS file.
244
+ if os.path.isabs(arg):
245
+ target = arg
246
+ else:
247
+ target = os.path.join(self.deps_dir, arg)
248
+
249
+ file_exists, fpath, forced_extension = files.get_source_file(target)
250
+ if file_exists:
251
+ _, file_ext = os.path.splitext(fpath)
252
+ if forced_extension or file_ext:
253
+ self.command_design_ref.add_file(fpath, caller_info=self.caller_info,
254
+ forced_extension=forced_extension)
255
+ unparsed.remove(arg)
256
+
257
+ else:
258
+ if not os.path.isdir(target) and \
259
+ self.command_design_ref.resolve_target_core(
260
+ target=target, no_recursion=False, caller_info=self.caller_info,
261
+ error_on_not_found=False
262
+ ):
263
+ unparsed.remove(arg)
188
264
 
189
265
  if unparsed:
190
266
  # This is only a warning - because things like CommandFlist may not have every
opencos/deps_schema.py CHANGED
@@ -85,6 +85,8 @@ my_target_name:
85
85
  directory path substitution relative to target dir
86
86
  (if substituted directory exists).
87
87
  tee: <---- string, filename to write logs to
88
+ run-after-tool: <---- bool, default false. Set to true to run after any
89
+ EDA tools, or any command handlers have completed.
88
90
  - work-dir-add-sources: <---- work-dir-add-sources, optional list (or string)
89
91
  - some_file_gen_from_sh.sv <---- string filename that we created with sh command
90
92
  - peakrdl: <---- string peakrdl command for CSR generation
@@ -171,6 +173,7 @@ DEPS_COMMANDS_LIST = [
171
173
  Optional('var-subst-args'): bool,
172
174
  Optional('var-subst-os-env'): bool,
173
175
  Optional('run-from-work-dir'): bool,
176
+ Optional('run-after-tool'): bool,
174
177
  Optional('filepath-subst-target-dir'): bool,
175
178
  Optional('dirpath-subst-target-dir'): bool,
176
179
  Optional('tee'): Or(str, type(None)),
opencos/eda.py CHANGED
@@ -520,7 +520,8 @@ def main(*args):
520
520
 
521
521
  original_args = args.copy() # save before any parsing.
522
522
 
523
- # Set global --debug, --quiet, --color early before parsing other args:
523
+ # Set global --debug, --quiet, --color, -f/--iput-file, --envfile
524
+ # early before parsing other args:
524
525
  util_parsed, unparsed = util.process_tokens(args)
525
526
 
526
527
  util.debug(f"main: file: {os.path.realpath(__file__)}")
opencos/eda_base.py CHANGED
@@ -230,6 +230,7 @@ class Command: # pylint: disable=too-many-public-methods
230
230
  'enable-tags': [],
231
231
  'disable-tags': [],
232
232
  'test-mode': False,
233
+ 'error-unknown-args': True,
233
234
  })
234
235
  self.args_help.update({
235
236
  'stop-before-compile': ('stop this run before any compile (if possible for tool) and'
@@ -249,6 +250,7 @@ class Command: # pylint: disable=too-many-public-methods
249
250
  ' --disable-tags has higher precedence than --enable-tags.'),
250
251
  'test-mode': ('command and tool dependent, usually stops the command early without'
251
252
  ' executing.'),
253
+ 'error-unknown-args': 'Enable errors on unknown/unparsable args',
252
254
  })
253
255
  self.modified_args = {}
254
256
  self.config = copy.deepcopy(config) # avoid external modifications.
@@ -495,9 +497,10 @@ class Command: # pylint: disable=too-many-public-methods
495
497
 
496
498
  # Do some minimal type handling, preserving the type(self.args[key])
497
499
  if key not in self.args:
498
- self.error(f'set_arg, {key=} not in self.args {value=}',
499
- f'({self.command_name=}, {self.__class__.__name__=})',
500
- error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
500
+ self.error_unknown_arg(
501
+ f'set_arg, {key=} not in self.args {value=}',
502
+ f'({self.command_name=}, {self.__class__.__name__=})'
503
+ )
501
504
 
502
505
  cur_value = self.args[key]
503
506
 
@@ -506,12 +509,11 @@ class Command: # pylint: disable=too-many-public-methods
506
509
  self.args[key].update(value)
507
510
 
508
511
  elif isinstance(cur_value, list):
509
- # if list, append (no duplicates)
512
+ # if list, append (allow duplicates)
510
513
  if isinstance(value, list):
511
514
  # new value also a list
512
515
  for x in value:
513
- if x not in self.args[key]:
514
- self.args[key].append(x)
516
+ self.args[key].append(x)
515
517
  elif value not in cur_value:
516
518
  self.args[key].append(value)
517
519
 
@@ -693,9 +695,10 @@ class Command: # pylint: disable=too-many-public-methods
693
695
  _, unparsed = self.run_argparser_on_list(tokens)
694
696
  if process_all and unparsed:
695
697
  self.warning_show_known_args()
696
- self.error(f"Didn't understand argument: '{unparsed=}' in",
697
- f" {self.command_name=} context, {pwd=}",
698
- error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
698
+ self.error_unknown_arg(
699
+ f"Didn't understand argument: '{unparsed=}' in",
700
+ f"{self.command_name=} context, {pwd=}"
701
+ )
699
702
 
700
703
  return unparsed
701
704
 
@@ -911,6 +914,16 @@ class Command: # pylint: disable=too-many-public-methods
911
914
  lines.append(self.pretty_str_known_args(command=commands[-1])) # use last command if > 1
912
915
  util.warning("\n".join(lines))
913
916
 
917
+ def error_unknown_arg(self, *msg) -> None:
918
+ '''For errors involving an unknown --arg, they can be optionally disabled
919
+
920
+ using --no-error-unknown-args
921
+ '''
922
+ if self.args['error-unknown-args']:
923
+ self.error(*msg, error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
924
+ else:
925
+ util.warning(*msg)
926
+
914
927
 
915
928
  class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
916
929
  '''CommandDesign is the eda base class for command handlers that need to track files.
@@ -970,13 +983,16 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
970
983
  self.targets_dict = {} # key = targets that we've already processed in DEPS files
971
984
  self.last_added_source_file_inferred_top = ''
972
985
 
973
- self.has_dep_shell_commands = False
986
+ self.has_pre_compile_dep_shell_commands = False
987
+ self.has_post_tool_dep_shell_commands = False
974
988
 
975
989
 
976
990
  def run_dep_commands(self) -> None:
977
- '''Run shell/peakrdl style commands from DEPS files
991
+ '''Run shell/peakrdl style commands from DEPS files, this is peformed before
978
992
 
979
- These are deferred to maintain the deps ordering, and run in that order.
993
+ any tool compile step. These are deferred to maintain the deps ordering, and
994
+ run in that order. Note this will NOT run any DEPS command marked with
995
+ run-after-tool=True.
980
996
  '''
981
997
  self.run_dep_shell_commands()
982
998
  # Update any work_dir_add_srcs@ in our self.files, self.files_v, etc, b/c
@@ -986,17 +1002,41 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
986
1002
  self.update_non_source_files_in_work_dir()
987
1003
 
988
1004
 
989
- def run_dep_shell_commands(self) -> None:
990
- '''Specifically runs shell command from DEPS files'''
1005
+ def run_post_tool_dep_commands(self) -> None:
1006
+ '''Run shell style commands from DEPS files that have been marked with
1007
+
1008
+ run-after-tool=True. Note these are skipped if any args like
1009
+ stop-before- or stop-after- are set.
1010
+ '''
1011
+
1012
+ self.run_dep_shell_commands(filter_run_after_tool=True)
1013
+
1014
+
1015
+ def run_dep_shell_commands( # pylint: disable=too-many-branches,too-many-locals
1016
+ self, filter_run_after_tool: bool = False
1017
+ ) -> None:
1018
+ '''Runs collected shell command from DEPS files.
1019
+
1020
+ There are two flavors of shell commands: with or without 'run-after-tool'
1021
+ set. The default is to run shell command before the compile step of any tool,
1022
+ by calling this method with default pre_compile=True before any tool runs
1023
+ (for generating code, etc). However, it may be useful to run shell commands
1024
+ after a tool is complete (check timing, coverage, etc).
1025
+ '''
991
1026
 
992
1027
  # Runs from self.args['work-dir']
993
1028
  all_cmds_lists = []
994
1029
 
995
1030
  log_fnames_count = {} # count per target_node.
996
1031
 
997
- for i, d in enumerate(self.dep_shell_commands):
1032
+ filtered_dep_shell_commands = []
1033
+ for value in self.dep_shell_commands:
1034
+ if value['attributes']['run-after-tool'] == filter_run_after_tool:
1035
+ filtered_dep_shell_commands.append(value)
1036
+
1037
+
1038
+ for i, d in enumerate(filtered_dep_shell_commands):
998
1039
  clist = util.ShellCommandList(d['exec_list'])
999
- run_from_work_dir = d['run_from_work_dir'] # default True
1000
1040
  log = clist.tee_fpath
1001
1041
  target_node = d["target_node"]
1002
1042
  if clist.tee_fpath is None:
@@ -1011,29 +1051,48 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1011
1051
  # (or tee name from DEPS.yml)
1012
1052
  [f'# command {i}: target: {d["target_path"]} : {target_node} --> {log}'],
1013
1053
  ]
1014
- if not run_from_work_dir:
1054
+ if not d['attributes']['run-from-work-dir']:
1015
1055
  all_cmds_lists.append([f'cd {d["target_path"]}'])
1016
1056
 
1017
1057
  # actual command (list or util.ShellCommandList)
1018
1058
  all_cmds_lists.append(clist)
1019
1059
 
1020
- if not run_from_work_dir:
1060
+ if not d['attributes']['run-from-work-dir']:
1021
1061
  all_cmds_lists.append([f'cd {os.path.abspath(self.args["work-dir"])}'])
1022
1062
 
1023
1063
  d['exec_list'] = clist # update to tee_fpath is set.
1024
1064
 
1025
1065
  if all_cmds_lists:
1066
+ if filter_run_after_tool:
1067
+ filename='post_tool_dep_shell_commands.sh'
1068
+ self.has_post_tool_dep_shell_commands = True
1069
+ else:
1070
+ filename='pre_compile_dep_shell_commands.sh'
1071
+ self.has_pre_compile_dep_shell_commands = True
1072
+
1026
1073
  util.write_shell_command_file(
1027
- dirpath=self.args['work-dir'], filename='pre_compile_dep_shell_commands.sh',
1074
+ dirpath=self.args['work-dir'], filename=filename,
1028
1075
  command_lists=all_cmds_lists
1029
1076
  )
1030
- self.has_dep_shell_commands = True
1031
1077
 
1032
- for i, d in enumerate(self.dep_shell_commands):
1078
+
1079
+ if all_cmds_lists and filter_run_after_tool and \
1080
+ any(self.args.get(x, False) for x in (
1081
+ "stop-before-compile",
1082
+ "stop-after-compile",
1083
+ "stop-after-elaborate"
1084
+ )):
1085
+ args_set = [key for key,value in self.args.items() if \
1086
+ key.startswith('stop-') and value]
1087
+ util.info(f'Skipping DEPS run-after-tool commands due to args {args_set}')
1088
+ util.debug(f'Skipped commands: {filtered_dep_shell_commands=}')
1089
+ return
1090
+
1091
+ for i, d in enumerate(filtered_dep_shell_commands):
1033
1092
  util.info(f'run_dep_shell_commands {i=}: {d=}')
1034
1093
  clist = util.ShellCommandList(d['exec_list'])
1035
1094
  tee_fpath=clist.tee_fpath
1036
- if d['run_from_work_dir']:
1095
+ if d['attributes']['run-from-work-dir']:
1037
1096
  run_from_dir = self.args['work-dir']
1038
1097
  else:
1039
1098
  # Run from the target's directory (not the `eda` caller $PWD)
@@ -1347,7 +1406,8 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1347
1406
  return self.resolve_target_core(target, no_recursion, caller_info)
1348
1407
 
1349
1408
  def resolve_target_core( # pylint: disable=too-many-locals,too-many-branches
1350
- self, target: str, no_recursion: bool, caller_info: str = ''
1409
+ self, target: str, no_recursion: bool, caller_info: str = '',
1410
+ error_on_not_found: bool = True
1351
1411
  ) -> bool:
1352
1412
  '''Returns True if target is found. recursive point for resolving path or DEPS markup
1353
1413
  target names.'''
@@ -1436,14 +1496,14 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1436
1496
  self.add_file(try_file, caller_info=f'n/a::{target}::n/a')
1437
1497
  found_target = True
1438
1498
  break # move on to the next target
1439
- if not found_target: # if STILL not found_this_target...
1499
+ if not found_target and error_on_not_found: # if STILL not found_this_target...
1440
1500
  self.error(f"Unable to resolve {target=}",
1441
1501
  error_code=status_constants.EDA_DEPS_TARGET_NOT_FOUND)
1442
1502
 
1443
1503
  # if we've found any target since being called, it means we found the one we were called for
1444
1504
  return found_target
1445
1505
 
1446
- def add_file(
1506
+ def add_file( # pylint: disable=too-many-locals,too-many-branches
1447
1507
  self, filename: str, use_abspath: bool = True, add_to_non_sources: bool = False,
1448
1508
  caller_info: str = '', forced_extension: str = ''
1449
1509
  ) -> str:
@@ -1467,6 +1527,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1467
1527
  vhdl_file_ext_list = known_file_ext_dict.get('vhdl', [])
1468
1528
  cpp_file_ext_list = known_file_ext_dict.get('cpp', [])
1469
1529
  sdc_file_ext_list = known_file_ext_dict.get('synth_constraints', [])
1530
+ dotf_file_ext_list = known_file_ext_dict.get('dotf', [])
1470
1531
 
1471
1532
  if forced_extension:
1472
1533
  # If forced_extension='systemverilog', then use the first known extension for
@@ -1499,6 +1560,13 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1499
1560
  elif file_ext in sdc_file_ext_list:
1500
1561
  self.files_sdc.append(file_abspath)
1501
1562
  util.debug(f"Added SDC file {filename} as {file_abspath}")
1563
+ elif file_ext in dotf_file_ext_list:
1564
+ # a stray .f file as a source file, sure why not support it:
1565
+ dp = DepsProcessor(command_design_ref=self, deps_entry={}, target='',
1566
+ target_path='', target_node='', deps_file='',
1567
+ caller_info=caller_info)
1568
+ dp.apply_args(args_list=[f'-f={file_abspath}'])
1569
+ del dp
1502
1570
  else:
1503
1571
  # unknown file extension. In these cases we link the file to the working directory
1504
1572
  # so it is available (for example, a .mem file that is expected to exist with relative
@@ -1592,9 +1660,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1592
1660
  if process_all and possible_unparsed_args:
1593
1661
  _tool = self.safe_which_tool()
1594
1662
  self.warning_show_known_args()
1595
- self.error(f"Didn't understand unparsed args: {possible_unparsed_args}, for command",
1596
- f"'{self.command_name}', tool '{_tool}'",
1597
- error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1663
+ self.error_unknown_arg(
1664
+ f"Didn't understand unparsed args: {possible_unparsed_args}, for command",
1665
+ f"'{self.command_name}', tool '{_tool}'"
1666
+ )
1598
1667
 
1599
1668
  remove_list = []
1600
1669
  last_potential_top_file = ('', '') # (top, fpath)
@@ -1652,9 +1721,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1652
1721
  # we were unable to figure out what this command line token is for...
1653
1722
  if process_all and unparsed:
1654
1723
  self.warning_show_known_args()
1655
- self.error(f"Didn't understand remaining args or targets {unparsed=} for command",
1656
- f"'{self.command_name}'",
1657
- error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
1724
+ self.error_unknown_arg(
1725
+ f"Didn't understand remaining args or targets {unparsed=} for command",
1726
+ f"'{self.command_name}'"
1727
+ )
1658
1728
 
1659
1729
  # handle a missing self.args['top'] with last filepath or last target:
1660
1730
  if not self.args.get('top', ''):
@@ -2199,9 +2269,10 @@ class CommandParallel(Command):
2199
2269
  bad_remaining_args = [x for x in single_cmd_unparsed if x.startswith('-')]
2200
2270
  if bad_remaining_args:
2201
2271
  self.warning_show_known_args(command=f'{self.command_name} {command}')
2202
- self.error(f'for {self.command_name} {command=} the following args are unknown',
2203
- f'{bad_remaining_args}',
2204
- error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
2272
+ self.error_unknown_arg(
2273
+ f'for {self.command_name} {command=} the following args are unknown',
2274
+ f'{bad_remaining_args}'
2275
+ )
2205
2276
 
2206
2277
  # Remove unparsed args starting with '+', since those are commonly sent downstream to
2207
2278
  # single job (example, CommandSim plusargs).
@@ -91,6 +91,9 @@ file_extensions:
91
91
  synth_constraints:
92
92
  - .sdc
93
93
  - .xdc
94
+ dotf:
95
+ - .f
96
+ - .vc
94
97
 
95
98
  inferred_top:
96
99
  # file extensions that we can infer "top" module from, if --top omitted.
@@ -229,6 +232,7 @@ tools:
229
232
  riviera:
230
233
  defines:
231
234
  OC_TOOL_RIVIERA: 1
235
+ RIVIERA: 1
232
236
  log-bad-strings:
233
237
  - "Error:"
234
238
  log-must-strings:
@@ -292,7 +296,7 @@ tools:
292
296
  - 3009 # 3009: [TSCALE] - Module 'myname' does not have a timeunit/timeprecision
293
297
  # specification in effect, but other modules do.
294
298
  simulate-waves-args: |
295
- -voptargs=+acc=bcgnprst
299
+ -voptargs=+acc=bcnprst
296
300
 
297
301
 
298
302
  iverilog:
opencos/files.py CHANGED
@@ -24,6 +24,7 @@ FORCE_PREFIX_DICT = {
24
24
  'vhdl@': 'vhdl',
25
25
  'cpp@': 'cpp',
26
26
  'sdc@': 'synth_constraints',
27
+ 'f@': 'dotf'
27
28
  }
28
29
 
29
30
  ALL_FORCED_PREFIXES = set(list(FORCE_PREFIX_DICT.keys()))
@@ -18,3 +18,14 @@ target_echo_hi_bye:
18
18
  target_test:
19
19
  deps: target_echo_hi_bye
20
20
  top: foo
21
+
22
+ target_test_with_post_tool_commands:
23
+ deps:
24
+ # In this test, we want to put a new command in the front of the ordered "deps" list,
25
+ # but with run-after-tool=true, so it should run after any of the normal pre-compile
26
+ # shell commands.
27
+ - commands:
28
+ - shell: echo "final goodbye"
29
+ run-after-tool: true
30
+ - target_echo_hi_bye
31
+ top: foo
opencos/tests/helpers.py CHANGED
@@ -13,6 +13,7 @@ from opencos import eda
13
13
  from opencos import deps_schema
14
14
  from opencos.utils.markup_helpers import yaml_safe_load
15
15
  from opencos.utils import status_constants
16
+ from opencos.utils.subprocess_helpers import subprocess_run_background
16
17
 
17
18
 
18
19
  def eda_wrap_is_sim_fail(rc: int, quiet: bool = False) -> bool:
@@ -157,10 +158,20 @@ class Helpers:
157
158
  DEFAULT_DIR = ''
158
159
  DEFAULT_LOG_DIR = os.getcwd()
159
160
  DEFAULT_LOG = os.path.join(DEFAULT_LOG_DIR, '.pytest.eda.log')
161
+
162
+ # How should the job run? subprocess? eda_wrap? eda.main?
163
+ # Note - if using eda.main, args like --debug will persist in opencos.util.args,
164
+ # so if you need those to be re-loaded, set RUN_IN_SUBPROCESS=True.
165
+ # Note - if you mess with os.enviorn, it may persist through subprocess.
166
+ RUN_IN_SUBPROCESS = True
167
+ USE_EDA_WRAP = True
168
+ PRESERVE_ENV = False
169
+
160
170
  def chdir(self):
161
171
  '''Changes directory to self.DEFAULT_DIR and removes eda.work, eda.export paths'''
162
172
  chdir_remove_work_dir('', self.DEFAULT_DIR)
163
173
 
174
+
164
175
  def _resolve_logfile(self, logfile=None) -> str:
165
176
  '''Returns the logfile's filepath'''
166
177
  ret = logfile
@@ -173,7 +184,11 @@ class Helpers:
173
184
  ret = os.path.join(self.DEFAULT_LOG_DIR, right)
174
185
  return ret
175
186
 
176
- def log_it(self, command_str:str, logfile=None, use_eda_wrap=True) -> int:
187
+ def log_it(
188
+ self, command_str: str, logfile=None, use_eda_wrap: bool = True,
189
+ run_in_subprocess: bool = False,
190
+ preserve_env: bool = False
191
+ ) -> int:
177
192
  '''Replacement for calling eda.main or eda_wrap, when you want an internal logfile
178
193
 
179
194
  Usage:
@@ -183,6 +198,10 @@ class Helpers:
183
198
  Note this will run with --no-default-log to avoid a Windows problem with stomping
184
199
  on a log file.
185
200
  '''
201
+
202
+ if self.PRESERVE_ENV or preserve_env:
203
+ saved_env = os.environ.copy()
204
+
186
205
  logfile = self._resolve_logfile(logfile)
187
206
  rc = 50
188
207
 
@@ -191,17 +210,30 @@ class Helpers:
191
210
  # look at eda.work/{target}.sim/sim.log or xsim.log.
192
211
  print(f'{os.getcwd()=}')
193
212
  print(f'{command_str=}')
194
- with open(logfile, 'w', encoding='utf-8') as f:
195
- with redirect_stdout(f), redirect_stderr(f):
196
- if use_eda_wrap:
197
- rc = eda_wrap('--no-default-log', *(command_str.split()))
198
- else:
199
- rc = eda.main('--no-default-log', *(command_str.split()))
200
- print(f'Wrote: {os.path.abspath(logfile)=}')
213
+ if run_in_subprocess or self.RUN_IN_SUBPROCESS:
214
+ command_list = ['eda', '--no-default-log'] + command_str.split()
215
+ _, _, rc = subprocess_run_background(
216
+ work_dir=self.DEFAULT_DIR,
217
+ command_list=command_list,
218
+ background=True,
219
+ tee_fpath=logfile
220
+ )
221
+ else:
222
+ with open(logfile, 'w', encoding='utf-8') as f:
223
+ with redirect_stdout(f), redirect_stderr(f):
224
+ if use_eda_wrap or self.USE_EDA_WRAP:
225
+ rc = eda_wrap('--no-default-log', *(command_str.split()))
226
+ else:
227
+ rc = eda.main('--no-default-log', *(command_str.split()))
228
+ print(f'Wrote: {os.path.abspath(logfile)=}')
229
+
230
+ if self.PRESERVE_ENV or preserve_env:
231
+ os.environ = saved_env
232
+
201
233
  return rc
202
234
 
203
235
  def is_in_log(self, *want_str, logfile=None, windows_path_support=False):
204
- '''Check if any of want_str args are in the logfile, or self.DEFAULT_LOG'''
236
+ '''Check if want_str (joined) is in the logfile, or self.DEFAULT_LOG'''
205
237
  logfile = self._resolve_logfile(logfile)
206
238
  want_str0 = ' '.join(list(want_str))
207
239
  want_str1 = want_str0.replace('/', '\\')
@@ -216,33 +248,31 @@ class Helpers:
216
248
  '''gets all log lines with any of want_str args are in the logfile, or self.DEFAULT_LOG'''
217
249
  logfile = self._resolve_logfile(logfile)
218
250
  ret_list = []
219
- want_str0 = ' '.join(list(want_str))
220
- want_str1 = want_str0.replace('/', '\\')
221
251
  with open(logfile, encoding='utf-8') as f:
222
252
  for line in f.readlines():
223
- if want_str0 in line:
253
+ if any(x in line for x in list(want_str)):
224
254
  ret_list.append(line)
225
- elif windows_path_support and want_str1 in line:
255
+ elif windows_path_support and \
256
+ any(x.replace('/', '\\') in line for x in list(want_str)):
226
257
  ret_list.append(line)
227
258
  return ret_list
228
259
 
229
260
  def get_log_words_with(self, *want_str, logfile=None, windows_path_support=False):
230
- '''gets all log lines with any of *want_str within a single word
261
+ '''gets all log words with any of *want_str within a single word
231
262
  in the logfile or self.DEFAULT_LOG
232
263
  '''
233
264
  logfile = self._resolve_logfile(logfile)
234
265
  ret_list = []
235
- want_str0 = ' '.join(list(want_str))
236
- want_str1 = want_str0.replace('/', '\\')
237
266
  with open(logfile, encoding='utf-8') as f:
238
267
  for line in f.readlines():
239
- if want_str0 in line:
268
+ if any(x in line for x in list(want_str)):
240
269
  for word in line.split():
241
- if want_str0 in word:
270
+ if any(x in word for x in list(want_str)):
242
271
  ret_list.append(word)
243
- elif windows_path_support and want_str1 in line:
272
+ elif windows_path_support and \
273
+ any(x.replace('/', '\\') in line for x in list(want_str)):
244
274
  for word in line.split():
245
- if want_str1 in word:
275
+ if any(x.replace('/', '\\') in word for x in list(want_str)):
246
276
  ret_list.append(word)
247
277
 
248
278
  return ret_list