opencos-eda 0.2.46__tar.gz → 0.2.48__tar.gz

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 (84) hide show
  1. {opencos_eda-0.2.46/opencos_eda.egg-info → opencos_eda-0.2.48}/PKG-INFO +1 -1
  2. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/multi.py +23 -5
  3. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/sweep.py +6 -2
  4. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/waves.py +1 -1
  5. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/deps_helpers.py +71 -28
  6. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/deps_schema.py +8 -0
  7. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda.py +9 -4
  8. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_base.py +58 -21
  9. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/helpers.py +24 -12
  10. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_deps_helpers.py +56 -19
  11. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_eda.py +11 -12
  12. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/vivado.py +18 -12
  13. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/util.py +131 -80
  14. {opencos_eda-0.2.46 → opencos_eda-0.2.48/opencos_eda.egg-info}/PKG-INFO +1 -1
  15. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/pyproject.toml +1 -1
  16. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/LICENSE +0 -0
  17. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/LICENSE.spdx +0 -0
  18. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/README.md +0 -0
  19. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/__init__.py +0 -0
  20. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/_version.py +0 -0
  21. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/_waves_pkg.sv +0 -0
  22. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/__init__.py +0 -0
  23. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/build.py +0 -0
  24. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/elab.py +0 -0
  25. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/export.py +0 -0
  26. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/flist.py +0 -0
  27. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/lec.py +0 -0
  28. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/open.py +0 -0
  29. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/proj.py +0 -0
  30. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/shell.py +0 -0
  31. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/sim.py +0 -0
  32. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/synth.py +0 -0
  33. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/targets.py +0 -0
  34. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/commands/upload.py +0 -0
  35. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_config.py +0 -0
  36. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_config_defaults.yml +0 -0
  37. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_config_max_verilator_waivers.yml +0 -0
  38. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_config_reduced.yml +0 -0
  39. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_deps_bash_completion.bash +0 -0
  40. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_extract_targets.py +0 -0
  41. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/eda_tool_helper.py +0 -0
  42. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/export_helper.py +0 -0
  43. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/export_json_convert.py +0 -0
  44. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/files.py +0 -0
  45. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/names.py +0 -0
  46. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/oc_cli.py +0 -0
  47. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/pcie.py +0 -0
  48. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/peakrdl_cleanup.py +0 -0
  49. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/seed.py +0 -0
  50. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/__init__.py +0 -0
  51. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/custom_config.yml +0 -0
  52. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/deps_files/command_order/DEPS.yml +0 -0
  53. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/deps_files/error_msgs/DEPS.yml +0 -0
  54. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -0
  55. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  56. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -0
  57. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -0
  58. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -0
  59. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_build.py +0 -0
  60. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_deps_schema.py +0 -0
  61. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_eda_elab.py +0 -0
  62. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_eda_synth.py +0 -0
  63. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_oc_cli.py +0 -0
  64. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tests/test_tools.py +0 -0
  65. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/__init__.py +0 -0
  66. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/invio.py +0 -0
  67. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/invio_helpers.py +0 -0
  68. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/invio_yosys.py +0 -0
  69. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/iverilog.py +0 -0
  70. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/modelsim_ase.py +0 -0
  71. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/questa.py +0 -0
  72. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/riviera.py +0 -0
  73. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/slang.py +0 -0
  74. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/slang_yosys.py +0 -0
  75. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/surelog.py +0 -0
  76. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/tabbycad_yosys.py +0 -0
  77. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/verilator.py +0 -0
  78. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos/tools/yosys.py +0 -0
  79. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos_eda.egg-info/SOURCES.txt +0 -0
  80. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos_eda.egg-info/dependency_links.txt +0 -0
  81. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos_eda.egg-info/entry_points.txt +0 -0
  82. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos_eda.egg-info/requires.txt +0 -0
  83. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/opencos_eda.egg-info/top_level.txt +0 -0
  84. {opencos_eda-0.2.46 → opencos_eda-0.2.48}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.46
3
+ Version: 0.2.48
4
4
  Summary: A simple Python package for wrapping RTL simuliatons and synthesis
5
5
  Author-email: Simon Sabato <simon@cognichip.ai>, Drew Ranck <drew@cognichip.ai>
6
6
  Project-URL: Homepage, https://github.com/cognichip/opencos
@@ -68,7 +68,7 @@ class CommandMulti(CommandParallel):
68
68
  return command, level
69
69
 
70
70
 
71
- def resolve_path_and_target_patterns(
71
+ def resolve_path_and_target_patterns( # pylint: disable=too-many-locals
72
72
  self, base_path: str, target: str, level: int = -1
73
73
  ) -> dict:
74
74
  '''Returns a dict of: key = matching path, value = set of matched targets.
@@ -91,10 +91,20 @@ class CommandMulti(CommandParallel):
91
91
 
92
92
  matching_targets_dict = {}
93
93
 
94
+ # Let's not glob.glob if the path_pattern and target_pattern are
95
+ # exact, aka if it does not have special characters for glob: * or ?
96
+ # for the target, we also support re, so: * + ?
97
+ if any(x in path_pattern for x in ['*', '?']):
98
+ paths_from_pattern = list(glob.glob(path_pattern, recursive=True))
99
+ else:
100
+ paths_from_pattern = [path_pattern]
101
+
102
+ target_pattern_needs_lookup = any(x in target_pattern for x in ['*', '?', '+'])
103
+
94
104
  # resolve the path_pattern portion using glob.
95
105
  # we'll have to check for DEPS markup files in path_pattern, to match the target_wildcard
96
106
  # using fnmatch or re.
97
- for path in glob.glob(path_pattern, recursive=True):
107
+ for path in paths_from_pattern:
98
108
 
99
109
  if self.path_hidden_or_work_dir(path):
100
110
  continue
@@ -108,7 +118,11 @@ class CommandMulti(CommandParallel):
108
118
  debug(f'in {rel_path=} looking for {target_pattern=} in {deps_targets=}')
109
119
 
110
120
  for t in deps_targets:
111
- if deps_helpers.fnmatch_or_re(pattern=target_pattern, string=t):
121
+ if target_pattern_needs_lookup:
122
+ matched = deps_helpers.fnmatch_or_re(pattern=target_pattern, string=t)
123
+ else:
124
+ matched = t == target_pattern
125
+ if matched:
112
126
  if rel_path not in matching_targets_dict:
113
127
  matching_targets_dict[rel_path] = set()
114
128
  matching_targets_dict[rel_path].add(t)
@@ -392,9 +406,13 @@ class CommandMulti(CommandParallel):
392
406
  # Special case for 'multi' --export-jsonl, run reach child with --export-json
393
407
  command_list.append('--export-json')
394
408
  if tool and len(all_multi_tools) > 1:
395
- command_list.append(f'--job-name={short_target}.{command}.{tool}')
409
+ jobname = f'{short_target}.{command}.{tool}'
396
410
  else:
397
- command_list.append(f'--job-name={short_target}.{command}')
411
+ jobname = f'{short_target}.{command}'
412
+ command_list.append(f'--job-name={jobname}')
413
+ logfile = os.path.join(self.args['eda-dir'], f'eda.{jobname}.log')
414
+ command_list.append(f'--force-logfile={logfile}')
415
+
398
416
 
399
417
  def append_jobs_from_targets(self, args:list):
400
418
  '''Helper method in CommandMulti to apply 'args' (list) to all self.targets,
@@ -206,14 +206,18 @@ class CommandSweep(CommandDesign, CommandParallel):
206
206
  snapshot_name = snapshot_name.replace(os.sep, '_') \
207
207
  + f'.{self.single_command}{sweep_string}'
208
208
  eda_path = get_eda_exec('sweep')
209
+ logfile = os.path.join(self.args['eda-dir'], f'eda.{snapshot_name}.log')
209
210
  self.jobs.append({
210
211
  'name' : snapshot_name,
211
212
  'index' : len(self.jobs),
212
213
  'command': self.single_command,
213
214
  'target': self.sweep_target,
214
215
  'command_list' : (
215
- [eda_path, self.single_command, self.sweep_target,
216
- f'--job-name={snapshot_name}'] + arg_tokens
216
+ [
217
+ eda_path, self.single_command, self.sweep_target,
218
+ f'--job-name={snapshot_name}',
219
+ f'--force-logfile={logfile}',
220
+ ] + arg_tokens
217
221
  )
218
222
  })
219
223
  return
@@ -143,7 +143,7 @@ class CommandWaves(CommandDesign):
143
143
  else:
144
144
  self.error(f"Don't know how to open {wave_file} without GtkWave in PATH")
145
145
  elif wave_file.endswith('.vcd'):
146
- if 'gtkwave' in self.config['tools_loaded'] and shutil.which('gktwave'):
146
+ if 'gtkwave' in self.config['tools_loaded'] and shutil.which('gtkwave'):
147
147
  command_list = ['gtkwave', wave_file]
148
148
  self.exec(os.path.dirname(wave_file), command_list)
149
149
  elif self._vsim_available(from_tools=self.VSIM_VCD_TOOLS):
@@ -68,7 +68,9 @@ class Defaults:
68
68
  'shell',
69
69
  'work-dir-add-srcs', 'work-dir-add-sources',
70
70
  'peakrdl',
71
- 'run-from-work-dir',
71
+ 'run-from-work-dir', # default True
72
+ 'filepath-subst-target-dir', # default True
73
+ 'dirpath-subst-target-dir', # default False
72
74
  'var-subst-args',
73
75
  'var-subst-os-env',
74
76
  'tee',
@@ -384,7 +386,13 @@ def deps_list_target_sanitize(entry, default_key:str='deps', target_node:str='',
384
386
  assert False, f"Can't convert to list {entry=} {default_key=} {target_node=} {deps_file=}"
385
387
 
386
388
 
387
- def path_substitutions_relative_to_work_dir(exec_list : list, info_str : str, target_path : str):
389
+ def path_substitutions_relative_to_work_dir(
390
+ exec_list: list, info_str: str, target_path: str,
391
+ enable_filepath_subst: bool, enable_dirpath_subst: bool
392
+ ) -> list:
393
+
394
+ if not enable_filepath_subst and not enable_dirpath_subst:
395
+ return exec_list
388
396
 
389
397
  # Look for path substitutions, b/c we later "work" in self.args['work-dir'], but
390
398
  # files should be relative to our target_path.
@@ -398,10 +406,18 @@ def path_substitutions_relative_to_work_dir(exec_list : list, info_str : str, ta
398
406
  # need this, and we can't assume dir levels in the work-dir.
399
407
  try:
400
408
  try_path = os.path.abspath(os.path.join(os.path.abspath(target_path), m.group(1)))
401
- if os.path.isfile(try_path) or os.path.isdir(try_path):
409
+ if enable_filepath_subst and os.path.isfile(try_path):
410
+ # make the substitution
411
+ exec_list[i] = word.replace(m.group(1), try_path)
412
+ debug(f'file path substitution {info_str=} {target_path=}: replaced - {word=}'
413
+ f'is now ={exec_list[i]}. This can be disabled in DEPS with:',
414
+ '"filepath-subst-targetdir: false"')
415
+ elif enable_dirpath_subst and os.path.isdir(try_path):
402
416
  # make the substitution
403
417
  exec_list[i] = word.replace(m.group(1), try_path)
404
- debug(f'path substitution {info_str=} {target_path=}: replaced - {word=} is now ={exec_list[i]}')
418
+ debug(f'dir path substitution {info_str=} {target_path=}: replaced - {word=}'
419
+ f'is now ={exec_list[i]}. This can be disabled in DEPS with:',
420
+ '"dirpath-subst-targetdir: false"')
405
421
  except:
406
422
  pass
407
423
 
@@ -1034,7 +1050,10 @@ class DepsProcessor:
1034
1050
 
1035
1051
 
1036
1052
 
1037
- def parse_deps_shell_str(line : str, target_path : str, target_node : str, enable : bool = True):
1053
+ def parse_deps_shell_str(line: str, target_path: str, target_node: str,
1054
+ enable_filepath_subst_target_dir: bool = True,
1055
+ enable_dirpath_subst_target_dir: bool = False,
1056
+ enable: bool = True) -> dict:
1038
1057
  '''Returns None or a dict of a possible shell command from line (str)
1039
1058
 
1040
1059
  Examples of 'line' str:
@@ -1050,25 +1069,30 @@ def parse_deps_shell_str(line : str, target_path : str, target_node : str, enabl
1050
1069
  target_node (str) -- from dependency parsing, the target containing this 'line' str.
1051
1070
  '''
1052
1071
  if not enable:
1053
- return None
1072
+ return {}
1054
1073
 
1055
1074
  m = re.match(r'^\s*shell\@(.*)\s*$', line)
1056
1075
  if not m:
1057
- return None
1076
+ return {}
1058
1077
 
1059
1078
  exec_str = m.group(1)
1060
1079
  exec_list = exec_str.split()
1061
1080
 
1062
1081
  # Look for path substitutions, b/c we later "work" in self.args['work-dir'], but
1063
1082
  # files should be relative to our target_path.
1064
- exec_list = path_substitutions_relative_to_work_dir(exec_list=exec_list, info_str='shell@', target_path=target_path)
1083
+ # Note this can be disable in DEPS via path-subst-target-dir=False
1084
+ exec_list = path_substitutions_relative_to_work_dir(
1085
+ exec_list=exec_list, info_str='shell@', target_path=target_path,
1086
+ enable_filepath_subst=enable_filepath_subst_target_dir,
1087
+ enable_dirpath_subst=enable_dirpath_subst_target_dir
1088
+ )
1065
1089
 
1066
- d = {'target_path': os.path.abspath(target_path),
1067
- 'target_node': target_node,
1068
- 'run_from_work_dir': True,
1069
- 'exec_list': exec_list,
1070
- }
1071
- return d
1090
+ return {
1091
+ 'target_path': os.path.abspath(target_path),
1092
+ 'target_node': target_node,
1093
+ 'run_from_work_dir': True, # may be overriden later.
1094
+ 'exec_list': exec_list,
1095
+ }
1072
1096
 
1073
1097
 
1074
1098
  def parse_deps_work_dir_add_srcs(line : str, target_path : str, target_node : str, enable : bool = True):
@@ -1100,8 +1124,12 @@ def parse_deps_work_dir_add_srcs(line : str, target_path : str, target_node : st
1100
1124
  return d
1101
1125
 
1102
1126
 
1103
- def parse_deps_peakrdl(line : str, target_path : str, target_node : str, enable : bool = True,
1104
- tool : str = ''):
1127
+ def parse_deps_peakrdl(
1128
+ line: str, target_path: str, target_node: str, enable: bool = True,
1129
+ enable_filepath_subst_target_dir: bool = True,
1130
+ enable_dirpath_subst_target_dir: bool = False,
1131
+ tool: str = ''
1132
+ ) -> dict:
1105
1133
  '''Returns None or a dict describing a PeakRDL CSR register generator dependency
1106
1134
 
1107
1135
  Examples of 'line' str:
@@ -1172,10 +1200,13 @@ def parse_deps_peakrdl(line : str, target_path : str, target_node : str, enable
1172
1200
  # Make these look like a dep_shell_command:
1173
1201
  for one_cmd_as_list in shell_commands:
1174
1202
  ret_dict['shell_commands_list'].append(
1175
- parse_deps_shell_str(line = ' shell@ ' + ' '.join(one_cmd_as_list),
1176
- target_path = target_path,
1177
- target_node = target_node
1178
- )
1203
+ parse_deps_shell_str(
1204
+ line=(' shell@ ' + ' '.join(one_cmd_as_list)),
1205
+ target_path=target_path,
1206
+ target_node=target_node,
1207
+ enable_filepath_subst_target_dir=enable_filepath_subst_target_dir,
1208
+ enable_dirpath_subst_target_dir=enable_dirpath_subst_target_dir
1209
+ )
1179
1210
  )
1180
1211
 
1181
1212
  # Make the work_dir_add_srcs dict:
@@ -1237,14 +1268,22 @@ def deps_commands_handler(config: dict, eda_args: dict,
1237
1268
  var_subst_dict = eda_args
1238
1269
 
1239
1270
  tee_fpath = command.get('tee', None)
1240
- run_from_work_dir = command.get('run-from-work-dir', True) # for shell, default True
1271
+
1272
+ # These are both optional bools, default True, would have to explicitly be set to False
1273
+ # to take effect:
1274
+ run_from_work_dir = command.get('run-from-work-dir', True)
1275
+ filepath_subst_target_dir = command.get('filepath-subst-target-dir', True)
1276
+ dirpath_subst_target_dir = command.get('dirpath-subst-target-dir', False)
1241
1277
 
1242
1278
  for key,item in command.items():
1243
1279
 
1244
- # skip the var-subst-* keys, since these types are bools
1245
- if key.startswith('var-subst') or \
1246
- key.startswith('tee') or \
1247
- key.startswith('run-from-work-dir'):
1280
+ # skip the tee and var-subst-* keys, since these types are bools and not commands.
1281
+ if key in ['tee',
1282
+ 'var-subst-os-env',
1283
+ 'var-subst-args',
1284
+ 'run-from-work-dir',
1285
+ 'filepath-subst-target-dir',
1286
+ 'dirpath-subst-target-dir']:
1248
1287
  continue
1249
1288
 
1250
1289
  # Optional variable substituion in commands
@@ -1255,9 +1294,11 @@ def deps_commands_handler(config: dict, eda_args: dict,
1255
1294
  if key == 'shell':
1256
1295
  # For now, piggyback on parse_deps_shell_str:
1257
1296
  ret_dict = parse_deps_shell_str(
1258
- line = 'shell@ ' + item,
1259
- target_path = target_path,
1260
- target_node = target_node,
1297
+ line=('shell@ ' + item),
1298
+ target_path=target_path,
1299
+ target_node=target_node,
1300
+ enable_filepath_subst_target_dir=filepath_subst_target_dir,
1301
+ enable_dirpath_subst_target_dir=dirpath_subst_target_dir,
1261
1302
  enable=config['dep_command_enables']['shell'],
1262
1303
  )
1263
1304
  # To support 'tee: <some-file>' need to append it to last
@@ -1286,6 +1327,8 @@ def deps_commands_handler(config: dict, eda_args: dict,
1286
1327
  line = 'peakrdl@ ' + item,
1287
1328
  target_path = target_path,
1288
1329
  target_node = target_node,
1330
+ enable_filepath_subst_target_dir=filepath_subst_target_dir,
1331
+ enable_dirpath_subst_target_dir=dirpath_subst_target_dir,
1289
1332
  enable=config['dep_command_enables']['peakrdl'],
1290
1333
  tool=eda_args.get('tool', '')
1291
1334
  )
@@ -71,6 +71,12 @@ my_target_name:
71
71
  var-subst-os-env: <---- bool, perform var substitution using os.environ
72
72
  run-from-work-dir: <---- bool, default True, if False runs from target dir
73
73
  instead of work-dir.
74
+ filepath-subst-target-dir: <---- bool, default True, if False does not perform
75
+ file path substitution relative to target dir
76
+ (if substituted file exists).
77
+ dirpath-subst-target-dir: <---- bool, default False, if True performs
78
+ directory path substitution relative to target dir
79
+ (if substituted directory exists).
74
80
  tee: <---- string, filename to write logs to
75
81
  - work-dir-add-sources: <---- work-dir-add-sources, optional list (or string)
76
82
  - some_file_gen_from_sh.sv <---- string filename that we created with sh command
@@ -153,6 +159,8 @@ DEPS_COMMANDS_LIST = [
153
159
  Optional('var-subst-args'): bool,
154
160
  Optional('var-subst-os-env'): bool,
155
161
  Optional('run-from-work-dir'): bool,
162
+ Optional('filepath-subst-target-dir'): bool,
163
+ Optional('dirpath-subst-target-dir'): bool,
156
164
  Optional('tee'): Or(str, type(None)),
157
165
  },
158
166
  {
@@ -18,9 +18,10 @@ from opencos import eda_config
18
18
  from opencos import eda_base
19
19
  from opencos.eda_base import Tool, which_tool
20
20
 
21
- # Globals
22
-
21
+ # Configure util:
23
22
  util.progname = "EDA"
23
+ util.global_log.default_log_enabled = True
24
+ util.global_log.default_log_filepath = os.path.join('eda.work', 'eda.log')
24
25
 
25
26
 
26
27
  # ******************************************************************************
@@ -453,10 +454,14 @@ def main(*args):
453
454
  if not config:
454
455
  util.error(f'eda.py main: problem loading config, {args=}')
455
456
  return 3
456
- return interactive(config=config)
457
+ main_ret = interactive(config=config)
457
458
  else:
458
- return process_tokens(tokens=list(unparsed), original_args=original_args,
459
+ main_ret = process_tokens(tokens=list(unparsed), original_args=original_args,
459
460
  config=config)
461
+ # Stop the util log, needed for pytests that call eda.main directly that otherwise
462
+ # won't close the log file via util's atexist.register(stop_log)
463
+ util.global_log.stop()
464
+ return main_ret
460
465
 
461
466
 
462
467
  def main_cli() -> None:
@@ -212,12 +212,30 @@ class Command:
212
212
  self.target = ""
213
213
  self.target_path = ""
214
214
  self.status = 0
215
+ self.errors_log_f = None
215
216
 
216
- def error(self, *args, **kwargs):
217
- '''Returns None, child classes can call self.error(..) instead of util.error, which updates their self.status.
217
+ def error(self, *args, **kwargs) -> None:
218
+ '''Returns None, child classes can call self.error(..) instead of util.error,
218
219
 
219
- Please consider using Command.error(..) (or self.error(..)) in place of util.error so self.status is updated.
220
+ which updates their self.status. Also will write out to eda.errors.log if the work-dir
221
+ exists. Please consider using Command.error(..) (or self.error(..)) in place of util.error
222
+ so self.status is updated.
220
223
  '''
224
+
225
+ if self.args['work-dir']:
226
+ if not self.errors_log_f:
227
+ try:
228
+ self.errors_log_f = open(
229
+ os.path.join(self.args['work-dir'], 'eda.errors.log'), 'w'
230
+ )
231
+ except:
232
+ pass
233
+ if self.errors_log_f:
234
+ print(
235
+ f'ERROR: [eda] ({self.command_name}) {" ".join(list(args))}',
236
+ file=self.errors_log_f
237
+ )
238
+
221
239
  self.status = util.error(*args, **kwargs)
222
240
 
223
241
  def status_any_error(self, report=True) -> bool:
@@ -1480,6 +1498,9 @@ class CommandParallel(Command):
1480
1498
  jobs_launched += 1
1481
1499
  anything_done = True
1482
1500
  job = self.jobs.pop(0)
1501
+ # TODO(drew): it might be nice to pass more items on 'job' dict, like
1502
+ # logfile or job-name, so CommandSweep or CommandMulti don't have to set
1503
+ # via args. on their command_list.
1483
1504
  if job['name'].startswith(multi_cwd): job['name'] = job['name'][len(multi_cwd):]
1484
1505
  # in all but fancy mode, we will print this text at the launch of a job. It may get a newline below
1485
1506
  job_text = sprint_job_line(jobs_launched, job['name'], hide_stats=run_parallel)
@@ -1663,34 +1684,36 @@ class CommandParallel(Command):
1663
1684
  '''Examines list self.jobs, and if leaf target names are duplicate will
1664
1685
  patch each command's job-name to:
1665
1686
  --job-name=path.leaf.command[.tool]
1687
+
1688
+ Also do for --force-logfile
1666
1689
  '''
1667
1690
 
1668
- def get_job_name(job_dict: dict) -> str:
1669
- '''Fishes the job-name out of an entry in self.jobs'''
1691
+ def get_job_arg(job_dict: dict, arg_name: str) -> str:
1692
+ '''Fishes the arg_name out of an entry in self.jobs'''
1670
1693
  for i, item in enumerate(job_dict['command_list']):
1671
- if item.startswith('--job-name='):
1672
- _, name = item.split('--job-name=')
1694
+ if item.startswith(f'--{arg_name}='):
1695
+ _, name = item.split(f'--{arg_name}=')
1673
1696
  return name
1674
- elif item == '--job-name':
1697
+ elif item == f'--{arg_name}':
1675
1698
  return job_dict['command_list'][i + 1]
1676
1699
  return ''
1677
1700
 
1678
- def replace_job_name(job_dict: dict, new_job_name: str) -> dict:
1679
- '''Replaces the job-name in an entry in self.jobs'''
1701
+ def replace_job_arg(job_dict: dict, arg_name: str, new_value: str) -> bool:
1702
+ '''Replaces the arg_name's value in an entry in self.jobs'''
1680
1703
  for i, item in enumerate(job_dict['command_list']):
1681
- if item.startswith('--job-name='):
1682
- job_dict['command_list'][i] = '--job-name=' + new_job_name
1683
- return job_dict
1684
- elif item == '--job-name':
1685
- job_dict['command_list'][i + 1] = new_job_name
1686
- return job_dict
1687
- return job_dict
1704
+ if item.startswith(f'--{arg_name}='):
1705
+ job_dict['command_list'][i] = f'--{arg_name}=' + new_value
1706
+ return True
1707
+ elif item == f'--{arg_name}':
1708
+ job_dict['command_list'][i + 1] = new_value
1709
+ return True
1710
+ return False
1688
1711
 
1689
1712
 
1690
1713
  job_names_count_dict = {}
1691
1714
  for job_dict in self.jobs:
1692
1715
 
1693
- key = get_job_name(job_dict)
1716
+ key = get_job_arg(job_dict, arg_name='job-name')
1694
1717
  if not key:
1695
1718
  self.error(f'{job_dict=} needs to have a --job-name= arg attached')
1696
1719
  if key not in job_names_count_dict:
@@ -1699,7 +1722,7 @@ class CommandParallel(Command):
1699
1722
  job_names_count_dict[key] += 1
1700
1723
 
1701
1724
  for i, job_dict in enumerate(self.jobs):
1702
- key = get_job_name(job_dict)
1725
+ key = get_job_arg(job_dict, 'job-name')
1703
1726
  if job_names_count_dict[key] < 2:
1704
1727
  continue
1705
1728
 
@@ -1709,6 +1732,20 @@ class CommandParallel(Command):
1709
1732
  patched_sub_work_dir = False
1710
1733
  patched_target_path = os.path.relpath(tpath).replace(os.sep, '_')
1711
1734
  new_job_name = f'{patched_target_path}.{key}'
1712
- new_job_dict = replace_job_name(job_dict, new_job_name)
1713
- self.jobs[i] = new_job_dict
1735
+ replace_job_arg(job_dict, arg_name='job-name', new_value=new_job_name)
1736
+
1737
+ # prepend path information to force-logfile (if present):
1738
+ force_logfile = get_job_arg(job_dict, arg_name='force-logfile')
1739
+ if force_logfile:
1740
+ left, right = os.path.split(force_logfile)
1741
+ new_force_logfile = os.path.join(left, f'{patched_target_path}.{right}')
1742
+ replace_job_arg(
1743
+ job_dict, arg_name='force-logfile', new_value=new_force_logfile
1744
+ )
1745
+ util.debug(
1746
+ f'Patched job {job_dict["name"]}: --force-logfile={new_force_logfile}'
1747
+ )
1748
+
1749
+
1750
+ self.jobs[i] = job_dict
1714
1751
  util.debug(f'Patched job {job_dict["name"]}: --job-name={new_job_name}')
@@ -124,20 +124,35 @@ def assert_export_jsonl_good(filepath:str, jsonl:bool=True) -> list:
124
124
  class Helpers:
125
125
  '''We do so much with logging in this file, might as well make it reusable'''
126
126
  DEFAULT_DIR = ''
127
- DEFAULT_LOG = 'eda.log'
127
+ DEFAULT_LOG_DIR = os.getcwd()
128
+ DEFAULT_LOG = os.path.join(DEFAULT_LOG_DIR, '.pytest.eda.log')
128
129
  def chdir(self):
129
130
  '''Changes directory to self.DEFAULT_DIR and removes eda.work, eda.export paths'''
130
131
  chdir_remove_work_dir('', self.DEFAULT_DIR)
131
132
 
133
+ def _resolve_logfile(self, logfile=None) -> str:
134
+ '''Returns the logfile's filepath'''
135
+ ret = logfile
136
+ if ret is None:
137
+ ret = self.DEFAULT_LOG
138
+ else:
139
+ left, right = os.path.split(logfile)
140
+ if not left or left in [os.path.sep, '.', '..']:
141
+ # relative logfile put in DEFAULT_LOG_DIR:
142
+ ret = os.path.join(self.DEFAULT_LOG_DIR, right)
143
+ return ret
144
+
132
145
  def log_it(self, command_str:str, logfile=None, use_eda_wrap=True) -> int:
133
146
  '''Replacement for calling eda.main or eda_wrap, when you want an internal logfile
134
147
 
135
148
  Usage:
136
- rc = self.log_it('sim foo', logfile='yes.log')
149
+ rc = self.log_it('sim foo')
137
150
  assert rc == 0
151
+
152
+ Note this will run with --no-default-log to avoid a Windows problem with stomping
153
+ on a log file.
138
154
  '''
139
- if logfile is None:
140
- logfile = self.DEFAULT_LOG
155
+ logfile = self._resolve_logfile(logfile)
141
156
  rc = 50
142
157
 
143
158
  # TODO(drew): There are some issues with log_it redirecting stdout from vivado
@@ -148,16 +163,15 @@ class Helpers:
148
163
  with open(logfile, 'w', encoding='utf-8') as f:
149
164
  with redirect_stdout(f), redirect_stderr(f):
150
165
  if use_eda_wrap:
151
- rc = eda_wrap(*(command_str.split()))
166
+ rc = eda_wrap('--no-default-log', *(command_str.split()))
152
167
  else:
153
- rc = eda.main(*(command_str.split()))
168
+ rc = eda.main('--no-default-log', *(command_str.split()))
154
169
  print(f'Wrote: {os.path.abspath(logfile)=}')
155
170
  return rc
156
171
 
157
172
  def is_in_log(self, *want_str, logfile=None, windows_path_support=False):
158
173
  '''Check if any of want_str args are in the logfile, or self.DEFAULT_LOG'''
159
- if logfile is None:
160
- logfile = self.DEFAULT_LOG
174
+ logfile = self._resolve_logfile(logfile)
161
175
  want_str0 = ' '.join(list(want_str))
162
176
  want_str1 = want_str0.replace('/', '\\')
163
177
  with open(logfile, encoding='utf-8') as f:
@@ -169,8 +183,7 @@ class Helpers:
169
183
 
170
184
  def get_log_lines_with(self, *want_str, logfile=None, windows_path_support=False):
171
185
  '''gets all log lines with any of want_str args are in the logfile, or self.DEFAULT_LOG'''
172
- if logfile is None:
173
- logfile = self.DEFAULT_LOG
186
+ logfile = self._resolve_logfile(logfile)
174
187
  ret_list = []
175
188
  want_str0 = ' '.join(list(want_str))
176
189
  want_str1 = want_str0.replace('/', '\\')
@@ -186,8 +199,7 @@ class Helpers:
186
199
  '''gets all log lines with any of *want_str within a single word
187
200
  in the logfile or self.DEFAULT_LOG
188
201
  '''
189
- if logfile is None:
190
- logfile = self.DEFAULT_LOG
202
+ logfile = self._resolve_logfile(logfile)
191
203
  ret_list = []
192
204
  want_str0 = ' '.join(list(want_str))
193
205
  want_str1 = want_str0.replace('/', '\\')