opencos-eda 0.2.42__tar.gz → 0.2.44__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 (82) hide show
  1. {opencos_eda-0.2.42/opencos_eda.egg-info → opencos_eda-0.2.44}/PKG-INFO +1 -1
  2. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/export.py +0 -1
  3. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/flist.py +4 -1
  4. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/deps_helpers.py +2 -0
  5. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda.py +4 -3
  6. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_base.py +54 -34
  7. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_config.py +1 -1
  8. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_config_defaults.yml +6 -2
  9. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/export_helper.py +6 -4
  10. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/files.py +1 -0
  11. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/helpers.py +2 -2
  12. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_eda_synth.py +63 -2
  13. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/iverilog.py +1 -1
  14. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/riviera.py +1 -1
  15. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/slang.py +1 -1
  16. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/surelog.py +1 -1
  17. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/verilator.py +5 -1
  18. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/vivado.py +22 -4
  19. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/yosys.py +32 -13
  20. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/util.py +37 -6
  21. {opencos_eda-0.2.42 → opencos_eda-0.2.44/opencos_eda.egg-info}/PKG-INFO +1 -1
  22. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/pyproject.toml +1 -1
  23. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/LICENSE +0 -0
  24. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/LICENSE.spdx +0 -0
  25. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/README.md +0 -0
  26. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/__init__.py +0 -0
  27. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/_version.py +0 -0
  28. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/_waves_pkg.sv +0 -0
  29. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/__init__.py +0 -0
  30. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/build.py +0 -0
  31. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/elab.py +0 -0
  32. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/multi.py +0 -0
  33. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/open.py +0 -0
  34. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/proj.py +0 -0
  35. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/sim.py +0 -0
  36. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/sweep.py +0 -0
  37. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/synth.py +0 -0
  38. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/targets.py +0 -0
  39. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/upload.py +0 -0
  40. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/commands/waves.py +0 -0
  41. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/deps_schema.py +0 -0
  42. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_config_max_verilator_waivers.yml +0 -0
  43. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_config_reduced.yml +0 -0
  44. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_deps_bash_completion.bash +0 -0
  45. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_extract_targets.py +0 -0
  46. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/eda_tool_helper.py +0 -0
  47. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/export_json_convert.py +0 -0
  48. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/names.py +0 -0
  49. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/oc_cli.py +0 -0
  50. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/pcie.py +0 -0
  51. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/peakrdl_cleanup.py +0 -0
  52. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/seed.py +0 -0
  53. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/__init__.py +0 -0
  54. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/custom_config.yml +0 -0
  55. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/deps_files/command_order/DEPS.yml +0 -0
  56. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/deps_files/error_msgs/DEPS.yml +0 -0
  57. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -0
  58. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  59. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -0
  60. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -0
  61. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -0
  62. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_build.py +0 -0
  63. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_deps_helpers.py +0 -0
  64. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_deps_schema.py +0 -0
  65. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_eda.py +0 -0
  66. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_eda_elab.py +0 -0
  67. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_oc_cli.py +0 -0
  68. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tests/test_tools.py +0 -0
  69. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/__init__.py +0 -0
  70. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/invio.py +0 -0
  71. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/invio_helpers.py +0 -0
  72. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/invio_yosys.py +0 -0
  73. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/modelsim_ase.py +0 -0
  74. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/questa.py +0 -0
  75. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/slang_yosys.py +0 -0
  76. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos/tools/tabbycad_yosys.py +0 -0
  77. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos_eda.egg-info/SOURCES.txt +0 -0
  78. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos_eda.egg-info/dependency_links.txt +0 -0
  79. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos_eda.egg-info/entry_points.txt +0 -0
  80. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos_eda.egg-info/requires.txt +0 -0
  81. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/opencos_eda.egg-info/top_level.txt +0 -0
  82. {opencos_eda-0.2.42 → opencos_eda-0.2.44}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.42
3
+ Version: 0.2.44
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
@@ -37,7 +37,6 @@ class CommandExport(CommandDesign):
37
37
  self, tokens: list, process_all: bool = True, pwd: str = os.getcwd()
38
38
  ) -> list:
39
39
 
40
- self.defines['OC_EXPORT'] = None
41
40
  unparsed = CommandDesign.process_tokens(
42
41
  self, tokens=tokens, process_all=process_all, pwd=pwd
43
42
  )
@@ -47,11 +47,14 @@ class CommandFList(CommandDesign):
47
47
  'no-quote-path' : False,
48
48
  'build-script' : "", # we don't want this to error either
49
49
 
50
- 'print-to-stdout': False, # do not save to file, print to stdout.
50
+ 'print-to-stdout': False,
51
51
 
52
52
  # ex: eda flist --print-to-stdout --emit-rel-path --quiet <target>
53
53
  'emit-rel-path' : False,
54
54
  })
55
+ self.args_help.update({
56
+ 'print-to-stdout': "do not save file, print to stdout",
57
+ })
55
58
 
56
59
  def process_tokens(
57
60
  self, tokens: list , process_all: bool = True, pwd: str = os.getcwd()
@@ -57,6 +57,8 @@ class Defaults:
57
57
  'with-tools',
58
58
  'with-args',
59
59
  'args',
60
+ 'deps',
61
+ 'reqs',
60
62
  'defines',
61
63
  'incdirs',
62
64
  'replace-config-tools',
@@ -320,9 +320,10 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
320
320
  for value in unparsed:
321
321
  if value in config['DEFAULT_HANDLERS'].keys():
322
322
  command = value
323
- if value in config['command_uses_no_tools']:
323
+ if not parsed.tool and value in config['command_tool_is_optional']:
324
+ # only do this if --tool was not set.
324
325
  run_auto_tool_setup = False
325
- unparsed.remove(value)
326
+ unparsed.remove(value) # remove command (flist, export, targets, etc)
326
327
  break
327
328
 
328
329
  if not interactive:
@@ -361,7 +362,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
361
362
  util.debug(f'{type(sco)=}')
362
363
  if not parsed.tool and \
363
364
  command not in config.get('command_determines_tool', []) and \
364
- command not in config.get('command_uses_no_tools', []):
365
+ command not in config.get('command_tool_is_optional', []):
365
366
  use_tool = which_tool(command, config)
366
367
  util.info(f"--tool not specified, using default for {command=}: {use_tool}")
367
368
 
@@ -686,6 +686,7 @@ class CommandDesign(Command):
686
686
  self.files_sv = []
687
687
  self.files_vhd = []
688
688
  self.files_cpp = []
689
+ self.files_sdc = []
689
690
  self.files_non_source = []
690
691
  self.files_caller_info = {}
691
692
  self.dep_shell_commands = [] # each list entry is a {}
@@ -696,6 +697,7 @@ class CommandDesign(Command):
696
697
 
697
698
  self.cached_deps = {} # key = abspath of DEPS markup file, has sub-dicts for 'data' and 'line_numbers'.
698
699
  self.targets_dict = {} # key = targets that we've already processed in DEPS files
700
+ self.last_added_source_file_inferred_top = ''
699
701
 
700
702
  def run_dep_commands(self):
701
703
  # Run any shell@ commands from DEPS files
@@ -759,7 +761,8 @@ class CommandDesign(Command):
759
761
  self.files.pop(key)
760
762
  self.files[new_key] = True
761
763
 
762
- my_file_lists_list = [self.files_v, self.files_sv, self.files_vhd, self.files_cpp]
764
+ my_file_lists_list = [self.files_v, self.files_sv, self.files_vhd, self.files_cpp,
765
+ self.files_sdc]
763
766
  for my_file_list in my_file_lists_list:
764
767
  for iter,value in enumerate(my_file_list):
765
768
  if value and type(value) is str and value.startswith(self._work_dir_add_srcs_path_string):
@@ -1028,6 +1031,7 @@ class CommandDesign(Command):
1028
1031
  sv_file_ext_list = known_file_ext_dict.get('systemverilog', [])
1029
1032
  vhdl_file_ext_list = known_file_ext_dict.get('vhdl', [])
1030
1033
  cpp_file_ext_list = known_file_ext_dict.get('cpp', [])
1034
+ sdc_file_ext_list = known_file_ext_dict.get('synth_constraints', [])
1031
1035
 
1032
1036
  if forced_extension:
1033
1037
  # If forced_extension='systemverilog', then use the first known extension for
@@ -1036,6 +1040,11 @@ class CommandDesign(Command):
1036
1040
  file_ext = known_file_ext_dict.get(forced_extension, [''])[0]
1037
1041
  util.debug(f"{forced_extension=} for {filename=} as type '{file_ext}'")
1038
1042
 
1043
+
1044
+ if not add_to_non_sources and \
1045
+ file_ext in known_file_ext_dict.get('inferred_top', []):
1046
+ self.last_added_source_file_inferred_top = file_abspath
1047
+
1039
1048
  if add_to_non_sources:
1040
1049
  self.files_non_source.append(file_abspath)
1041
1050
  util.debug("Added non-source file file %s as %s" % (filename, file_abspath))
@@ -1051,6 +1060,9 @@ class CommandDesign(Command):
1051
1060
  elif file_ext in cpp_file_ext_list:
1052
1061
  self.files_cpp.append(file_abspath)
1053
1062
  util.debug("Added C++ file %s as %s" % (filename, file_abspath))
1063
+ elif file_ext in sdc_file_ext_list:
1064
+ self.files_sdc.append(file_abspath)
1065
+ util.debug("Added SDC file %s as %s" % (filename, file_abspath))
1054
1066
  else:
1055
1067
  # unknown file extension. In these cases we link the file to the working directory
1056
1068
  # so it is available (for example, a .mem file that is expected to exist with relative path)
@@ -1114,8 +1126,8 @@ class CommandDesign(Command):
1114
1126
 
1115
1127
  # by this point hopefully this is a target ... is it a simple filename?
1116
1128
  remove_list = []
1117
- last_potential_top = None # used for 'top' if top not specified.
1118
- last_potential_top_path = None
1129
+ last_potential_top_file = ('', '') # (top, fpath)
1130
+ last_potential_top_target = ('', '') # (top, path/to/full-target-name)
1119
1131
  last_potential_top_isfile = False
1120
1132
  caller_info = ''
1121
1133
  for token in unparsed:
@@ -1131,21 +1143,13 @@ class CommandDesign(Command):
1131
1143
  else:
1132
1144
  self.add_file(filename=fpath, caller_info=caller_info,
1133
1145
  forced_extension=forced_extension)
1134
-
1135
- known_file_ext_dict = self.config.get('file_extensions', {})
1136
- if forced_extension:
1137
- # If forced_extension='systemverilog', then use the first known extension for
1138
- # it ('.sv', from eda_config_defaults.yml):
1139
- file_ext = known_file_ext_dict.get(forced_extension, [''])[0]
1140
-
1141
- if not self.args['top'] and \
1142
- file_ext and \
1143
- file_ext in known_file_ext_dict.get('inferred_top', []):
1144
- # if we haven't yet been given a top, or inferred one, we take the last one we get
1145
- # from a raw list of RTL file names (from args or command line tokens)
1146
- last_potential_top_path = file_abspath
1147
- last_potential_top = self.get_top_name(file_abspath)
1148
- last_potential_top_isfile = True
1146
+ if not self.args['top']:
1147
+ known_file_ext_dict = self.config.get('file_extensions', {})
1148
+ if forced_extension:
1149
+ file_ext = known_file_ext_dict.get(forced_extension, [''])[0]
1150
+ if file_ext in known_file_ext_dict.get('inferred_top', []):
1151
+ # last cmd line arg was a filename that could have inferred top.
1152
+ last_potential_top_isfile = True
1149
1153
 
1150
1154
  remove_list.append(token)
1151
1155
  continue # done with token, consume it, we added the file.
@@ -1158,12 +1162,9 @@ class CommandDesign(Command):
1158
1162
 
1159
1163
  util.debug(f'Calling self.resolve_target on {target_name=}')
1160
1164
  if self.resolve_target(target_name, caller_info=caller_info):
1161
- if self.args['top'] == '':
1162
- # if we haven't yet been given a top, or inferred one, we take the last named target
1163
- # from args or command line tokens
1164
- # from a target name
1165
- last_potential_top = self.get_top_name(target_name)
1166
- last_potential_top_path = target_name
1165
+ if not self.args['top']:
1166
+ # last cmd line arg was a target that we'll likely use for inferred top.
1167
+ last_potential_top_target = (self.get_top_name(target_name), target_name)
1167
1168
  last_potential_top_isfile = False
1168
1169
 
1169
1170
  remove_list.append(token)
@@ -1176,16 +1177,35 @@ class CommandDesign(Command):
1176
1177
  if process_all and len(unparsed) > 0:
1177
1178
  self.error(f"Didn't understand command remaining tokens {unparsed=} in CommandDesign")
1178
1179
 
1179
- # handle a missing self.args['top'] with last filepath.
1180
- if self.args.get('top', '') == '' and last_potential_top is not None:
1181
- self.args['top'] = last_potential_top
1182
- self.args['top-path'] = last_potential_top_path
1183
- util.info(f"Inferred --top {self.args['top']} {self.args['top-path']}")
1184
- if last_potential_top_isfile:
1185
- # top wasn't set, we're using the final command-line 'arg' filename (not from DEPS.yml)
1186
- # need to override self.target if that was set. Otherwise it won't save to the correct
1187
- # work-dir:
1188
- self.target = last_potential_top
1180
+ # handle a missing self.args['top'] with last filepath or last target:
1181
+ if not self.args.get('top', ''):
1182
+ if not last_potential_top_isfile and last_potential_top_target[0]:
1183
+ # If we have a target name from DEPS, prefer to use that.
1184
+ self.args['top'], self.args['top-path'] = last_potential_top_target
1185
+ util.info("--top not specified, inferred from target:",
1186
+ f"{self.args['top']} ({self.args['top-path']})")
1187
+
1188
+ else:
1189
+ best_top_fname = self.last_added_source_file_inferred_top
1190
+ if best_top_fname:
1191
+ last_potential_top_file = (self.get_top_name(best_top_fname), best_top_fname)
1192
+
1193
+ if not self.args['top'] and last_potential_top_file[0]:
1194
+ # If we don't have a target name, and no top name yet, then go looking for the
1195
+ # module name in the final source file added.
1196
+ self.args['top-path'] = last_potential_top_file[1] # from tuple: (top, fpath)
1197
+ self.args['top'] = util.get_inferred_top_module_name(
1198
+ module_guess=last_potential_top_file[0],
1199
+ module_fpath=last_potential_top_file[1]
1200
+ )
1201
+ if self.args['top']:
1202
+ util.info("--top not specified, inferred from final source file:",
1203
+ f"{self.args['top']} ({self.args['top-path']})")
1204
+ # If top wasn't set, and we're using the final command-line 'arg' filename
1205
+ # (not from DEPS.yml) we need to override self.target if that was set. Otherwise
1206
+ # it won't save to the correct work-dir:
1207
+ self.target = self.args['top']
1208
+
1189
1209
  if self.error_on_missing_top and not self.args.get('top', ''):
1190
1210
  self.error(f"Did not get a --top or DEPS top, required to run command",
1191
1211
  f"'{self.command_name}' for tool={self.args.get('tool', None)}")
@@ -30,7 +30,7 @@ class Defaults:
30
30
  'vars',
31
31
  'file_extensions',
32
32
  'command_determines_tool',
33
- 'command_uses_no_tools',
33
+ 'command_tool_is_optional',
34
34
  'tools',
35
35
  'auto_tools_order',
36
36
  ])
@@ -60,6 +60,9 @@ file_extensions:
60
60
  - .vhdl
61
61
  cpp:
62
62
  - .cpp
63
+ synth_constraints:
64
+ - .sdc
65
+ - .xdc
63
66
 
64
67
  inferred_top:
65
68
  # file extensions that we can infer "top" module from, if --top omitted.
@@ -72,8 +75,9 @@ command_determines_tool:
72
75
  # eda commands that will self-determine the tool to use
73
76
  - waves
74
77
 
75
- command_uses_no_tools:
76
- # eda commands that do not use a tool at all, will skip auto_tools_order
78
+ command_tool_is_optional:
79
+ # eda commands that may not need to use a tool at all, will skip auto_tools_order if --tool=None (default)
80
+ - flist
77
81
  - export
78
82
  - targets
79
83
 
@@ -352,23 +352,25 @@ class ExportHelper:
352
352
  }
353
353
  }
354
354
 
355
- if len(deps_file_args) > 0:
355
+
356
+ if deps_file_args:
356
357
  data[self.target]['args'] = deps_file_args.copy()
357
358
 
358
359
  if self.args.get('top', None):
359
360
  data[self.target]['top'] = self.args['top']
360
361
 
361
- if len(self.cmd_design_obj.defines.keys()) > 0:
362
+ if self.cmd_design_obj.defines:
362
363
  data[self.target]['defines'] = self.cmd_design_obj.defines.copy()
363
364
  for define in _remove_DEPS_yml_defines:
364
365
  # Remove defines keys for OC_ROOT and OC_SEED. Change OC_SEED to _ORIG_OC_SEED
365
366
  if define in data[self.target]['defines']:
366
367
  data[self.target]['defines'].pop(define)
367
368
 
368
- if len(self.cmd_design_obj.files_non_source) > 0:
369
+ reqs_fullpath_list = self.included_files + self.cmd_design_obj.files_non_source
370
+ if reqs_fullpath_list:
369
371
  # Need to strip path information from non-source files:
370
372
  data[self.target]['reqs'] = list()
371
- for fullpath in self.cmd_design_obj.files_non_source:
373
+ for fullpath in reqs_fullpath_list:
372
374
  filename = os.path.split(fullpath)[1]
373
375
  data[self.target]['reqs'].append(filename)
374
376
 
@@ -23,6 +23,7 @@ FORCE_PREFIX_DICT = {
23
23
  'v@': 'verilog',
24
24
  'vhdl@': 'vhdl',
25
25
  'cpp@': 'cpp',
26
+ 'sdc@': 'synth_constraints',
26
27
  }
27
28
 
28
29
  ALL_FORCED_PREFIXES = set(list(FORCE_PREFIX_DICT.keys()))
@@ -19,7 +19,7 @@ def can_run_eda_command(*commands, config: dict) -> bool:
19
19
  if not handler:
20
20
  return False
21
21
  if handler and getattr(handler, 'CHECK_REQUIRES', []):
22
- if not all(isinstance(handler, x) for x in getattr(handler, 'CHECK_REQUIRES', [])):
22
+ if not all(issubclass(handler, x) for x in getattr(handler, 'CHECK_REQUIRES', [])):
23
23
  return False
24
24
  runnable.append(True)
25
25
  return runnable and all(runnable)
@@ -128,7 +128,7 @@ class Helpers:
128
128
  '''Changes directory to self.DEFAULT_DIR and removes eda.work, eda.export paths'''
129
129
  chdir_remove_work_dir('', self.DEFAULT_DIR)
130
130
 
131
- def log_it(self, command_str:str, logfile=None, use_eda_wrap=True):
131
+ def log_it(self, command_str:str, logfile=None, use_eda_wrap=True) -> int:
132
132
  '''Replacement for calling eda.main or eda_wrap, when you want an internal logfile
133
133
 
134
134
  Usage:
@@ -1,17 +1,20 @@
1
1
  '''pytests for: eda [multi|tools-multi] synth [args] <target(s)>'''
2
2
 
3
3
  import os
4
+ import shutil
5
+
4
6
  import pytest
5
7
 
6
8
  from opencos import eda, eda_tool_helper
7
9
  from opencos.tests import helpers
10
+ from opencos.tests.helpers import Helpers
8
11
 
9
12
 
10
- thispath = os.path.dirname(__file__)
13
+ THISPATH = os.path.dirname(__file__)
11
14
 
12
15
  def chdir_remove_work_dir(relpath):
13
16
  '''Changes dir to relpath, removes the work directories (eda.work, eda.export*)'''
14
- return helpers.chdir_remove_work_dir(thispath, relpath)
17
+ return helpers.chdir_remove_work_dir(THISPATH, relpath)
15
18
 
16
19
  # Figure out what tools the system has available, without calling eda.main(..)
17
20
  config, tools_loaded = eda_tool_helper.get_config_and_tools_loaded()
@@ -75,3 +78,61 @@ class Tests:
75
78
  rc = eda.main(*cmdlist)
76
79
  print(f'{rc=}')
77
80
  assert rc == 0
81
+
82
+
83
+ def vivado_has_xpms() -> bool:
84
+ '''Returns True if Vivado is installed and has visibility to XPMs'''
85
+ if 'vivado' not in tools_loaded:
86
+ return False
87
+ vivado_exe = shutil.which('vivado')
88
+ vivado_bin_path, _ = os.path.split(vivado_exe)
89
+ vivado_base_path, _ = os.path.split(vivado_bin_path) # strip bin/vivado
90
+
91
+ return os.path.exists(os.path.join(vivado_base_path, 'data', 'ip', 'xpm'))
92
+
93
+
94
+ @pytest.mark.skipif(
95
+ 'slang_yosys' not in tools_loaded, reason="requires slang_yosys for synth"
96
+ )
97
+ class TestsSlangYosys(Helpers):
98
+ '''Tests that require tool=slang_yosys to be available'''
99
+
100
+ def test_sdc_file(self):
101
+ '''Test for 'eda synth' on oclib_fifo_with_sdc
102
+
103
+ This does not use the actual .sdc file, but it also shouldn't fail with
104
+ that file in the 'deps' list (CommandDesign should track it correctly)
105
+ '''
106
+ chdir_remove_work_dir('deps_files/test_sdc_files')
107
+ cmd_str = 'synth --tool=slang_yosys oclib_fifo_with_sdc'
108
+ rc = self.log_it(cmd_str, use_eda_wrap=False)
109
+ assert rc == 0
110
+
111
+ @pytest.mark.skipif('vivado' not in tools_loaded, reason="requires vivado")
112
+ @pytest.mark.skipif(not vivado_has_xpms(), reason="requires install to have XPMs")
113
+ class TestsVivado(Helpers):
114
+ '''Tests that require tool=vivado with XPMs available for synthesis'''
115
+
116
+ def test_sdc_file(self):
117
+ '''Test for 'eda synth' on oclib_fifo_with_sdc
118
+
119
+ And check that the .sdc file was used and not the default generated .xdc
120
+ file
121
+ '''
122
+ chdir_remove_work_dir('deps_files/test_sdc_files')
123
+ cmd_str = 'synth --tool=vivado oclib_fifo_with_sdc'
124
+ rc = self.log_it(cmd_str, use_eda_wrap=False)
125
+ assert rc == 0
126
+
127
+ # apparently this doesn't get saved in the eda.log. That's not great,
128
+ # but we can check the <target>.synth.log file.
129
+ synth_log = os.path.join(
130
+ THISPATH, 'deps_files', 'test_sdc_files',
131
+ 'eda.work', 'oclib_fifo_with_sdc.synth', 'oclib_fifo.synth.log'
132
+ )
133
+ sdc_lines = self.get_log_lines_with('.sdc', logfile=synth_log)
134
+ assert sdc_lines
135
+ for sdc_line in sdc_lines:
136
+ assert 'oclib_fifo_vivado.sdc' in sdc_line
137
+ assert 'test_sdc_files' in sdc_line
138
+ assert 'eda.work' not in sdc_line
@@ -37,7 +37,7 @@ class ToolIverilog(Tool):
37
37
  iverilog_version_ret = subprocess.run(
38
38
  [self.iverilog_exe, '-v'], capture_output=True, check=False
39
39
  )
40
- lines = iverilog_version_ret.stdout.decode("utf-8").split('\n')
40
+ lines = iverilog_version_ret.stdout.decode("utf-8", errors="replace").split('\n')
41
41
  words = lines[0].split() # 'Icarus Verilog version 13.0 (devel) (s20221226-568-g62727e8b2)'
42
42
  version = words[3]
43
43
  util.debug(f'{iverilog_path=} {lines[0]=}')
@@ -36,7 +36,7 @@ class ToolRiviera(ToolModelsimAse):
36
36
  capture_output=True,
37
37
  check=False
38
38
  )
39
- stdout = version_ret.stdout.decode('utf-8').rstrip()
39
+ stdout = version_ret.stdout.decode('utf-8', errors='replace').rstrip()
40
40
 
41
41
  # Expect:
42
42
  # Aldec, Inc. Riviera-PRO version 2025.04.139.9738 built for Linux64 on May 30, 2025
@@ -44,7 +44,7 @@ class ToolSlang(Tool):
44
44
  capture_output=True,
45
45
  check=False
46
46
  )
47
- stdout = version_ret.stdout.decode('utf-8')
47
+ stdout = version_ret.stdout.decode('utf-8', errors='replace')
48
48
  util.debug(f'{path=} {version_ret=}')
49
49
  words = stdout.split() # slang version 8.0.6+b4a74b00
50
50
  if len(words) < 3:
@@ -33,7 +33,7 @@ class ToolSurelog(Tool):
33
33
  version_ret = subprocess.run(
34
34
  [self.surelog_exe, '--version'], capture_output=True, check=False
35
35
  )
36
- stdout = version_ret.stdout.decode('utf-8')
36
+ stdout = version_ret.stdout.decode('utf-8', errors='replace')
37
37
  util.debug(f'{path=} {version_ret=}')
38
38
  words = stdout.split() # VERSION: 1.84 (first line)
39
39
  if len(words) < 2:
@@ -49,7 +49,7 @@ class ToolVerilator(Tool):
49
49
  capture_output=True,
50
50
  check=False
51
51
  )
52
- stdout = version_ret.stdout.decode('utf-8')
52
+ stdout = version_ret.stdout.decode('utf-8', errors='replace')
53
53
  util.debug(f'{path=} {version_ret=}')
54
54
  words = stdout.split() # 'Verilator 5.027 devel rev v5.026-92-g403a197e2
55
55
  if len(words) < 1:
@@ -166,6 +166,10 @@ class VerilatorSim(CommandSim, ToolVerilator):
166
166
  # don't run this if we're stopping before/after compile/elab
167
167
  return
168
168
 
169
+ if not os.path.isfile(os.path.join(self.args['work-dir'], 'obj_dir', 'sim.exe')):
170
+ self.error('Verilated executable obj_dir/sim.exe does not exist')
171
+ return
172
+
169
173
  # Note that this is not returning a pass/fail bash return code,
170
174
  # so we will likely have to log-scrape to deterimine pass/fail.
171
175
  # Also, if we ran in cc-mode, we will not get the "R e p o r t: Verilator"
@@ -70,7 +70,7 @@ class ToolVivado(Tool):
70
70
  #try:
71
71
  # # Get version from vivado -version, or xsim --version:
72
72
  # vivado_ret = subprocess.run(['vivado', '-version'], capture_output=True)
73
- # lines = vivado_ret.stdout.decode('utf-8').split('\n')
73
+ # lines = vivado_ret.stdout.decode('utf-8', errors='replace').split('\n')
74
74
  # words = lines[0].split() # vivado v2024.2.1 (64-bit)
75
75
  # version = words[1][1:] # 2024.2.1
76
76
  # self._VERSION = version
@@ -388,9 +388,14 @@ class CommandSynthVivado(CommandSynth, ToolVivado):
388
388
  self.exec(self.args['work-dir'], command_list)
389
389
 
390
390
 
391
- def write_tcl_file(self, tcl_file: str) -> None:
391
+ def write_tcl_file( # pylint: disable=too-many-locals,too-many-branches
392
+ self, tcl_file: str
393
+ ) -> None:
392
394
  '''Writes synthesis capable Vivado tcl file to filepath 'tcl_file'.'''
393
395
 
396
+ # TODO(drew): This method needs to be broken up to avoid the pylint
397
+ # waivers.
398
+
394
399
  v = self.get_vivado_tcl_verbose_arg()
395
400
 
396
401
  defines = ""
@@ -416,9 +421,12 @@ class CommandSynthVivado(CommandSynth, ToolVivado):
416
421
  part = self.args['part']
417
422
  top = self.args['top']
418
423
 
424
+ default_xdc = False
419
425
  if self.args['xdc'] != "":
420
- default_xdc = False
421
426
  xdc_file = os.path.abspath(self.args['xdc'])
427
+ elif self.files_sdc:
428
+ # Use files from DEPS target or command line.
429
+ xdc_file = ''
422
430
  else:
423
431
  default_xdc = True
424
432
  xdc_file = os.path.abspath(os.path.join(self.args['work-dir'],
@@ -427,7 +435,17 @@ class CommandSynthVivado(CommandSynth, ToolVivado):
427
435
 
428
436
  tcl_lines += [
429
437
  f"create_fileset -constrset constraints_1 {v}",
430
- f"add_files -fileset constraints_1 {xdc_file} {v}",
438
+ ]
439
+ for _file in self.files_sdc:
440
+ # NOTE - sdc files cannot (yet) be attached to other modules.
441
+ tcl_lines += [
442
+ f"add_files -fileset constraints_1 {_file} {v}",
443
+ ]
444
+ if xdc_file:
445
+ tcl_lines += [
446
+ f"add_files -fileset constraints_1 {xdc_file} {v}",
447
+ ]
448
+ tcl_lines += [
431
449
  "# FIRST PASS -- auto_detect_xpm",
432
450
  "synth_design -rtl -rtl_skip_ip -rtl_skip_constraints -no_timing_driven -no_iobuf " \
433
451
  + f"-top {top} {incdirs} {defines} {v}",
@@ -46,7 +46,7 @@ class ToolYosys(Tool):
46
46
  [self.sta_exe, '-version'], capture_output=True, check=False
47
47
  )
48
48
  util.debug(f'{self.yosys_exe} {sta_version_ret=}')
49
- sta_ver = sta_version_ret.stdout.decode('utf-8').split()[0]
49
+ sta_ver = sta_version_ret.stdout.decode('utf-8', errors='replace').split()[0]
50
50
  if sta_ver:
51
51
  self.sta_version = sta_ver
52
52
 
@@ -56,7 +56,7 @@ class ToolYosys(Tool):
56
56
  util.debug(f'{self.yosys_exe} {version_ret=}')
57
57
 
58
58
  # Yosys 0.48 (git sha1 aaa534749, clang++ 14.0.0-1ubuntu1.1 -fPIC -O3)
59
- words = version_ret.stdout.decode('utf-8').split()
59
+ words = version_ret.stdout.decode('utf-8', errors='replace').split()
60
60
 
61
61
  if len(words) < 2:
62
62
  self.error(f'{self.yosys_exe} --version: returned unexpected str {version_ret=}')
@@ -93,16 +93,27 @@ class CommonSynthYosys(CommandSynth, ToolYosys):
93
93
  'yosys-blackbox': [], # list of modules that yosys will blackbox.
94
94
  })
95
95
  self.args_help.update({
96
- 'sta': 'After running Yosys, run "sta" with --liberty-file.' \
97
- + ' sta can be installed via: https://github.com/The-OpenROAD-Project/OpenSTA',
98
- 'sdc-file': '.sdc file to use with --sta, if not present will use auto constraints',
99
- 'liberty-file': 'Single liberty file for synthesis and sta,' \
100
- + ' for example: github/OpenSTA/examples/nangate45_slow.lib.gz',
96
+ 'sta': (
97
+ 'After running Yosys, run "sta" with --liberty-file.'
98
+ ' sta can be installed via: https://github.com/The-OpenROAD-Project/OpenSTA'
99
+ ),
100
+ 'sdc-file': (
101
+ '.sdc file to use with --sta, if not present will use auto constraints.'
102
+ ' Note you can have .sdc files in "deps" of DEPS.yml targets.'
103
+ ),
104
+ 'liberty-file': (
105
+ 'Single liberty file for synthesis and sta,'
106
+ ' for example: github/OpenSTA/examples/nangate45_slow.lib.gz'
107
+ ),
101
108
  'yosys-synth': 'The synth command provided to Yosys, see: yosys help.',
102
- 'yosys-pre-synth': 'Yosys commands performed prior to running "synth"' \
103
- + ' (or eda arg value for --yosys-synth)',
104
- 'yosys-blackbox': 'List of modules that yosys will blackbox, likely will need these' \
105
- + ' in Verilog-2001 for yosys to read outside of slang and synth',
109
+ 'yosys-pre-synth': (
110
+ 'Yosys commands performed prior to running "synth"'
111
+ ' (or eda arg value for --yosys-synth)'
112
+ ),
113
+ 'yosys-blackbox': (
114
+ 'List of modules that yosys will blackbox, likely will need these'
115
+ ' in Verilog-2001 for yosys to read outside of slang and synth'
116
+ ),
106
117
  })
107
118
 
108
119
  self.yosys_out_dir = ''
@@ -193,6 +204,9 @@ class CommonSynthYosys(CommandSynth, ToolYosys):
193
204
  # Need to create sta.f:
194
205
  if self.args['sdc-file']:
195
206
  sdc_path = self.args['sdc-file']
207
+ elif self.files_sdc:
208
+ # Use files from DEPS target or command line.
209
+ sdc_path = ''
196
210
  else:
197
211
  # Need to create sdc.f:
198
212
  sdc_path = 'sdc.f'
@@ -204,9 +218,14 @@ class CommonSynthYosys(CommandSynth, ToolYosys):
204
218
  'read_liberty ' + self.args['liberty-file'],
205
219
  'read_verilog ' + self.yosys_v_path,
206
220
  'link_design ' + self.args['top'],
207
- 'read_sdc ' + sdc_path,
208
- 'report_checks',
209
221
  ]
222
+ for _file in self.files_sdc:
223
+ lines.append('read_sdc ' + _file)
224
+ if sdc_path:
225
+ lines.append('read_sdc ' + sdc_path)
226
+
227
+ lines.append('report_checks')
228
+
210
229
  f.write('\n'.join(lines))
211
230
 
212
231
  return util.ShellCommandList(
@@ -33,11 +33,10 @@ args = {
33
33
  'errors' : 0,
34
34
  }
35
35
 
36
-
37
- def strip_all_quotes(s:str):
36
+ def strip_all_quotes(s: str) -> str:
38
37
  return s.replace("'", '').replace('"', '')
39
38
 
40
- def strip_outer_quotes(s:str):
39
+ def strip_outer_quotes(s: str) -> str:
41
40
  ret = str(s)
42
41
  while (ret.startswith("'") and ret.endswith("'")) or \
43
42
  (ret.startswith('"') and ret.endswith('"')):
@@ -635,6 +634,38 @@ def write_eda_config_and_args(dirpath : str, filename='eda_output_config.yml', c
635
634
  yaml_safe_writer(data=data, filepath=fullpath)
636
635
 
637
636
 
637
+ def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
638
+ '''Returns the best guess as the 'top' module name name, given a fpath where
639
+
640
+ module_fpath = /some/path/to/module_guess.[v|sv]
641
+
642
+ Use the module_guess if it exists in the file as: module <module_guess>
643
+ Otherwise use the last observed: module <best_guess>
644
+ Otherwise use blank str
645
+ '''
646
+
647
+ best_guess = ''
648
+ if not os.path.isfile(module_fpath):
649
+ return ''
650
+ with open(module_fpath, encoding='utf-8') as f:
651
+ for line in f.readlines():
652
+ line = line.strip()
653
+ if line.startswith('module '):
654
+ parts = line.split()
655
+ module_name = parts[1]
656
+ rstrip_nonword_pattern = r'\W+$'
657
+ module_name = re.sub(rstrip_nonword_pattern, '', module_name)
658
+ if bool(re.fullmatch(r'^\w+$', module_name)):
659
+ if module_name == module_guess:
660
+ return module_guess
661
+ elif module_name:
662
+ best_guess = module_name
663
+ if best_guess:
664
+ return best_guess
665
+ else:
666
+ return ''
667
+
668
+
638
669
  def subprocess_run(work_dir, command_list, fake:bool=False, shell=False) -> int:
639
670
  ''' Run command_list in the foreground, with preference to use bash if shell=True.'''
640
671
 
@@ -700,7 +731,7 @@ def subprocess_run_background(work_dir, command_list, background=True, fake:bool
700
731
  stderr = ''
701
732
  with open(tee_fpath, 'w') as f:
702
733
  for line in iter(proc.stdout.readline, b''):
703
- line = line.rstrip().decode("utf-8")
734
+ line = line.rstrip().decode("utf-8", errors="replace")
704
735
  if not background:
705
736
  print(line)
706
737
  f.write(line + '\n')
@@ -717,8 +748,8 @@ def subprocess_run_background(work_dir, command_list, background=True, fake:bool
717
748
  stdout, stderr = proc.communicate()
718
749
  rc = proc.returncode
719
750
 
720
- stdout = stdout.decode('utf-8') if stdout else ""
721
- stderr = stderr.decode('utf-8') if stderr else ""
751
+ stdout = stdout.decode('utf-8', errors="replace") if stdout else ""
752
+ stderr = stderr.decode('utf-8', errors="replace") if stderr else ""
722
753
  debug(f"shell_run_background: {rc=}")
723
754
  if stdout:
724
755
  for lineno, line in enumerate(stdout.strip().split('\n')):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.42
3
+ Version: 0.2.44
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [project]
4
4
  name = "opencos-eda"
5
- version = "0.2.42"
5
+ version = "0.2.44"
6
6
  dependencies = [
7
7
  # opencos/eda.py dependencies
8
8
  "mergedeep >= 1.3.4",
File without changes
File without changes
File without changes
File without changes