opencos-eda 0.3.10__py3-none-any.whl → 0.3.12__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 (59) hide show
  1. opencos/commands/deps_help.py +63 -113
  2. opencos/commands/export.py +7 -2
  3. opencos/commands/multi.py +4 -4
  4. opencos/commands/sim.py +14 -15
  5. opencos/commands/sweep.py +1 -1
  6. opencos/commands/synth.py +1 -2
  7. opencos/commands/upload.py +192 -4
  8. opencos/commands/waves.py +52 -8
  9. opencos/deps/deps_commands.py +6 -6
  10. opencos/deps/deps_processor.py +129 -50
  11. opencos/docs/Architecture.md +45 -0
  12. opencos/docs/ConnectingApps.md +29 -0
  13. opencos/docs/DEPS.md +199 -0
  14. opencos/docs/Debug.md +77 -0
  15. opencos/docs/DirectoryStructure.md +22 -0
  16. opencos/docs/Installation.md +117 -0
  17. opencos/docs/OcVivadoTcl.md +63 -0
  18. opencos/docs/OpenQuestions.md +7 -0
  19. opencos/docs/README.md +13 -0
  20. opencos/docs/RtlCodingStyle.md +54 -0
  21. opencos/docs/eda.md +147 -0
  22. opencos/docs/oc_cli.md +135 -0
  23. opencos/eda.py +358 -155
  24. opencos/eda_base.py +187 -60
  25. opencos/eda_config.py +70 -35
  26. opencos/eda_config_defaults.yml +310 -186
  27. opencos/eda_config_reduced.yml +19 -39
  28. opencos/eda_tool_helper.py +190 -21
  29. opencos/files.py +26 -1
  30. opencos/tools/cocotb.py +11 -23
  31. opencos/tools/invio.py +2 -2
  32. opencos/tools/invio_yosys.py +2 -1
  33. opencos/tools/iverilog.py +3 -3
  34. opencos/tools/modelsim_ase.py +1 -1
  35. opencos/tools/quartus.py +172 -137
  36. opencos/tools/questa_common.py +50 -9
  37. opencos/tools/riviera.py +5 -4
  38. opencos/tools/slang.py +14 -10
  39. opencos/tools/slang_yosys.py +1 -0
  40. opencos/tools/surelog.py +7 -6
  41. opencos/tools/verilator.py +9 -7
  42. opencos/tools/vivado.py +315 -180
  43. opencos/tools/yosys.py +5 -5
  44. opencos/util.py +6 -3
  45. opencos/utils/dict_helpers.py +31 -0
  46. opencos/utils/markup_helpers.py +2 -2
  47. opencos/utils/str_helpers.py +38 -0
  48. opencos/utils/subprocess_helpers.py +3 -3
  49. opencos/utils/vscode_helper.py +2 -2
  50. opencos/utils/vsim_helper.py +16 -5
  51. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/METADATA +1 -1
  52. opencos_eda-0.3.12.dist-info/RECORD +93 -0
  53. opencos/eda_config_max_verilator_waivers.yml +0 -39
  54. opencos_eda-0.3.10.dist-info/RECORD +0 -81
  55. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/WHEEL +0 -0
  56. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/entry_points.txt +0 -0
  57. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/licenses/LICENSE +0 -0
  58. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/licenses/LICENSE.spdx +0 -0
  59. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/top_level.txt +0 -0
opencos/eda_base.py CHANGED
@@ -30,6 +30,7 @@ from opencos.utils.str_helpers import sprint_time, strip_outer_quotes, string_or
30
30
  from opencos.utils.subprocess_helpers import subprocess_run_background
31
31
  from opencos.utils import status_constants
32
32
 
33
+ from opencos.files import safe_shutil_which
33
34
  from opencos.deps.deps_file import DepsFile, deps_data_get_all_targets
34
35
  from opencos.deps.deps_processor import DepsProcessor
35
36
 
@@ -114,7 +115,7 @@ def get_eda_exec(command: str = '') -> str:
114
115
  # packages cannot be run standalone, they need to be called as: python3 -m opencos.eda,
115
116
  # and do not work with relative paths. This only works if env OC_ROOT is set or can be found.
116
117
  # 3. If you ran 'source bin/addpath' then you are always using the local (opencos repo)/bin/eda
117
- eda_path = shutil.which('eda')
118
+ eda_path = safe_shutil_which('eda')
118
119
  if not eda_path:
119
120
  # Can we run from OC_ROOT/bin/eda?
120
121
  oc_root = util.get_oc_root()
@@ -210,11 +211,11 @@ class Tool:
210
211
 
211
212
  def set_exe(self, config: dict) -> None:
212
213
  '''Sets self._EXE based on config'''
213
- if self._TOOL and self._TOOL in config.get('auto_tools_order', [{}])[0]:
214
+ if self._TOOL and self._TOOL in config.get('auto_tools_found', {}):
214
215
  # config['auto_tools_found'] has the first exe full path:
215
216
  exe = config.get('auto_tools_found', {}).get(self._TOOL)
216
217
  if exe and exe != self._EXE:
217
- # Note that shutil.which() on the exe leaf may not match, this does NOT
218
+ # Note that safe_shutil_which() on the exe leaf may not match, this does NOT
218
219
  # modify os.environ['PATH'].
219
220
  util.debug(f'{self._TOOL} using exe: {exe}')
220
221
  self._EXE = exe
@@ -230,7 +231,11 @@ class Tool:
230
231
  return str(self._TOOL) + ':' + str(self._VERSION)
231
232
 
232
233
  def get_versions(self) -> str:
233
- '''Sets and returns self._VERSION'''
234
+ '''Sets and returns self._VERSION. Note that this is overriden by nearly all
235
+
236
+ child Tool classes, and should limit info/warning messaging since it is part
237
+ of opencos.eda.init_config(..)
238
+ '''
234
239
  return self._VERSION
235
240
 
236
241
  def report_tool_warn_error_counts(self) -> None:
@@ -240,14 +245,18 @@ class Tool:
240
245
  return
241
246
 
242
247
  info_color = Colors.green
248
+ error_color = Colors.bgreen
243
249
  start = ''
244
- if self.tool_error_count or self.tool_warning_count:
245
- start = safe_emoji('🔶 ')
250
+ if self.tool_warning_count:
251
+ info_color = Colors.yellow
252
+ if self.tool_error_count:
246
253
  info_color = Colors.yellow
254
+ error_color = Colors.bred
255
+ start = safe_emoji('🔶 ')
247
256
  util.info(
248
- f"{start}Tool - {tool_name}, total counts:",
257
+ f"{start}Tool - {Colors.bold}{tool_name}{Colors.normal}{info_color}, total counts:",
249
258
  f"{Colors.bold}{self.tool_warning_count} tool warnings{Colors.normal}{info_color},",
250
- f"{Colors.bold}{self.tool_error_count} tool errors",
259
+ f"{error_color}{self.tool_error_count} tool errors",
251
260
  color=info_color
252
261
  )
253
262
 
@@ -272,6 +281,8 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
272
281
  self.args_help = {}
273
282
  if getattr(self, 'args_kwargs', None) is None:
274
283
  self.args_kwargs = {}
284
+ if getattr(self, 'args_args', None) is None:
285
+ self.args_args = {} # support for multiple named --arg that map to same self.args key.
275
286
  self.args.update({
276
287
  "keep" : False,
277
288
  "force" : False,
@@ -358,6 +369,7 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
358
369
  self.errors_log_f = None
359
370
  self.auto_tool_applied = False
360
371
  self.tool_changed_respawn = {}
372
+ self.top_details = {}
361
373
 
362
374
 
363
375
  def get_info_job_name(self) -> str:
@@ -558,8 +570,23 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
558
570
  # Set the util.artifacts path with our work-dir:
559
571
  util.artifacts.set_artifacts_json_dir(self.args['work-dir'])
560
572
 
573
+ # Since work-dir has now safely been created (or error flagged) we can override the
574
+ # inferred 'top' if top was not specified (aka, we guessed based on target name or
575
+ # file/module name)
576
+ self.update_top_if_inferred()
577
+
561
578
  return self.args['work-dir']
562
579
 
580
+
581
+ def update_top_if_inferred(self) -> None:
582
+ '''Child classes can override (CommandDesign does)
583
+
584
+ Idea is to overwrite self.args['top'] if it was inferred, but Command.args
585
+ does not initially set self.args['top'], so this method takes no action.
586
+ '''
587
+ return
588
+
589
+
563
590
  def artifacts_add(self, name: str, typ: str, description: str) -> None:
564
591
  '''Adds a file to util.artifacts, derived classes may override'''
565
592
  util.artifacts.add(name=name, typ=typ, description=description)
@@ -710,6 +737,9 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
710
737
  arguments = [] # list supplied to parser.add_argument(..) so one liner supports both.
711
738
  for this_key in keys:
712
739
  arguments.append(f'--{this_key}')
740
+ for arg in self.args_args.get(this_key, []):
741
+ if arg not in arguments:
742
+ arguments.append(f'--{arg}')
713
743
 
714
744
  if self.args_help.get(key, ''):
715
745
  help_kwargs = {'help': self.args_help[key] + f' (default={value})'}
@@ -838,6 +868,9 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
838
868
  '''
839
869
 
840
870
  _, unparsed = self.run_argparser_on_list(tokens)
871
+
872
+ self._apply_deps_file_defaults_if_present()
873
+
841
874
  if process_all and unparsed:
842
875
  self.warning_show_known_args()
843
876
  self.error_ifarg(
@@ -849,23 +882,20 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
849
882
 
850
883
  return unparsed
851
884
 
852
- def get_command_from_unparsed_args(
853
- self, tokens: list, error_if_no_command: bool = True
885
+ def get_sub_command_from_config(
886
+ self, error_if_no_command: bool = True,
854
887
  ) -> str:
855
- '''Given a list of unparsed args, try to fish out the eda COMMAND value.
888
+ '''If this is a command with a sub-command (multi, sweep), then opencos.eda would
856
889
 
857
- This will remove the value from the tokens list.
890
+ have already parsed in an place it in config['sub_command']. Get this value and return
891
+ it.
858
892
  '''
859
- ret = ''
860
- for value in tokens:
861
- if value in self.config['command_handler'].keys():
862
- ret = value
863
- tokens.remove(value)
864
- break
893
+ ret = self.config.get('sub_command', '')
865
894
 
866
895
  if not ret and error_if_no_command:
896
+ util.debug(f'get_sub_command_from_config: see: {self.config=}')
867
897
  self.error(f"Looking for a valid eda {self.command_name} COMMAND",
868
- f"but didn't find one in {tokens=}",
898
+ "but didn't find one in config",
869
899
  error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
870
900
  return ret
871
901
 
@@ -983,37 +1013,39 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
983
1013
  color.disable() # strip our color object if < 3.14
984
1014
 
985
1015
 
1016
+ # Include -G, +incdir+, +define+ help if this is a CommanDesign class:
986
1017
  lines.append('')
987
- lines.append(
988
- f" {color.cyan}-G{color.byellow}<parameterName>{color.normal}=" \
989
- + f"{color.yellow}<value>{color.normal}"
990
- )
991
- lines.append(indent_me((
992
- " Add parameter to top level, support bit/int/string types only."
993
- " Example: -GDEPTH=8 (DEPTH treated as SV int/integer)."
994
- " -GENABLE=1 (ENABLED treated as SV bit/int/integer)."
995
- " -GName=eda (Name treated as SV string \"eda\")."
996
- )))
997
-
998
- lines.append(f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}")
999
- lines.append(indent_me((
1000
- " Add define w/out value to tool ahead of SV sources"
1001
- " Example: +define+SIM_SPEEDUP"
1002
- )))
1003
- lines.append(
1004
- f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}=" \
1005
- + f"{color.yellow}<value>{color.normal}")
1006
- lines.append(indent_me((
1007
- " Add define w/ value to tool ahead of SV sources"
1008
- " Example: +define+TECH_LIB=2 +define+FULL_NAME=\"E D A\""
1009
- )))
1010
- lines.append(f" {color.cyan}+incdir+{color.byellow}PATH{color.normal}")
1011
- lines.append(indent_me((
1012
- " Add path (absolute or relative) for include directories"
1013
- " for SystemVerilog `include \"<some-file>.svh\""
1014
- " Example: +incdir+../lib"
1015
- )))
1016
- lines.append('')
1018
+ if isinstance(self, CommandDesign):
1019
+ lines.append(
1020
+ f" {color.cyan}-G{color.byellow}<parameterName>{color.normal}=" \
1021
+ + f"{color.yellow}<value>{color.normal}"
1022
+ )
1023
+ lines.append(indent_me((
1024
+ " Add parameter to top level, support bit/int/string types only."
1025
+ " Example: -GDEPTH=8 (DEPTH treated as SV int/integer)."
1026
+ " -GENABLE=1 (ENABLED treated as SV bit/int/integer)."
1027
+ " -GName=eda (Name treated as SV string \"eda\")."
1028
+ )))
1029
+
1030
+ lines.append(f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}")
1031
+ lines.append(indent_me((
1032
+ " Add define w/out value to tool ahead of SV sources"
1033
+ " Example: +define+SIM_SPEEDUP"
1034
+ )))
1035
+ lines.append(
1036
+ f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}=" \
1037
+ + f"{color.yellow}<value>{color.normal}")
1038
+ lines.append(indent_me((
1039
+ " Add define w/ value to tool ahead of SV sources"
1040
+ " Example: +define+TECH_LIB=2 +define+FULL_NAME=\"E D A\""
1041
+ )))
1042
+ lines.append(f" {color.cyan}+incdir+{color.byellow}PATH{color.normal}")
1043
+ lines.append(indent_me((
1044
+ " Add path (absolute or relative) for include directories"
1045
+ " for SystemVerilog `include \"<some-file>.svh\""
1046
+ " Example: +incdir+../lib"
1047
+ )))
1048
+ lines.append('')
1017
1049
 
1018
1050
  for line in lines:
1019
1051
  print(line)
@@ -1082,6 +1114,13 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
1082
1114
  else:
1083
1115
  util.warning(*msg)
1084
1116
 
1117
+ @staticmethod
1118
+ def get_top_name(name: str) -> str:
1119
+ '''Attempt to get the 'top' module name from a file, such as path/to/mine.sv will
1120
+
1121
+ return "mine"'''
1122
+ return os.path.splitext(os.path.basename(name))[0]
1123
+
1085
1124
 
1086
1125
  def update_tool_warn_err_counts_from_log_lines(
1087
1126
  self, log_lines: list, bad_strings: list, warning_strings: list
@@ -1103,6 +1142,48 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
1103
1142
  setattr(self, 'tool_warning_count', getattr(self, 'tool_warning_count', 0) + 1)
1104
1143
 
1105
1144
 
1145
+ def _apply_deps_file_defaults_if_present(self) -> None:
1146
+ '''Only runs if this is not a CommandDesign class.
1147
+
1148
+ Looks at DEPS file in current directory, and will attempt to apply 'DEFAULTS' if present
1149
+ '''
1150
+
1151
+ if isinstance(self, CommandDesign):
1152
+ return
1153
+
1154
+ # Try to fish out DEFAULTS from ./DEPS if present, so we can apply args to self.
1155
+ _cache_none = {}
1156
+ data = None
1157
+ if self.config['deps_markup_supported']:
1158
+ deps = DepsFile(
1159
+ command_design_ref=self, target_path=str(Path('.')), cache=_cache_none
1160
+ )
1161
+ data = deps.data
1162
+
1163
+ entry = None
1164
+ if data is not None and 'DEFAULTS' in data:
1165
+ entry = deps.get_entry(target_node=str(Path('./DEFAULTS')))
1166
+ if not entry:
1167
+ return
1168
+
1169
+ target = str(Path('./DEFAULTS'))
1170
+ _, target_node = os.path.split(target)
1171
+ caller_info = deps.gen_caller_info(target)
1172
+
1173
+ deps_processor = DepsProcessor(
1174
+ command_design_ref=self,
1175
+ deps_entry=entry,
1176
+ target=target,
1177
+ target_path=str(Path('.')),
1178
+ target_node=target_node,
1179
+ deps_file = deps.deps_file,
1180
+ caller_info = caller_info
1181
+ )
1182
+ _ = deps_processor.process_deps_entry()
1183
+
1184
+
1185
+
1186
+
1106
1187
  class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1107
1188
  '''CommandDesign is the eda base class for command handlers that need to track files.
1108
1189
 
@@ -1161,11 +1242,22 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1161
1242
  # keys 'data' and 'line_numbers'
1162
1243
  self.cached_deps = {}
1163
1244
  self.targets_dict = {} # key = targets that we've already processed in DEPS files
1164
- self.last_added_source_file_inferred_top = ''
1165
1245
 
1166
1246
  self.has_pre_compile_dep_shell_commands = False
1167
1247
  self.has_post_tool_dep_shell_commands = False
1168
1248
 
1249
+ self.top_details = {
1250
+ # used by CommandDesign to track last file added.
1251
+ 'last_added_source_file': '',
1252
+ # implies top was not set, we inferred from the final command line
1253
+ # DEPS target name
1254
+ 'inferred_from_target_name': False,
1255
+ # implies top was not set, we inferred from the final source file added,
1256
+ # based on a 'module' name we parsed.
1257
+ # also implies self.target was set from inferred top:
1258
+ 'inferred_from_last_added_source_file': False,
1259
+ }
1260
+
1169
1261
 
1170
1262
  def run_dep_commands(self) -> None:
1171
1263
  '''Run shell/peakrdl style commands from DEPS files, this is peformed before
@@ -1346,13 +1438,43 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1346
1438
  else:
1347
1439
  os.symlink(src=fname, dst=destfile)
1348
1440
 
1349
- @staticmethod
1350
- def get_top_name(name: str) -> str:
1351
- '''Attempt to get the 'top' module name from a file, such as path/to/mine.sv will
1352
1441
 
1353
- return "mine"'''
1354
- # TODO(drew): Use the helper method in util for this instead to peek in file contents?
1355
- return os.path.splitext(os.path.basename(name))[0]
1442
+ def update_top_if_inferred(self) -> None:
1443
+ '''Overridden from Command, uses self.top_details - attempts to overwrite
1444
+
1445
+ self.args['top'] if it was previously set based on the first DEPS target name,
1446
+ so it more accurately reflects the top-module-name. You should only do this if the
1447
+ self.args['work-dir'] has already been determined.
1448
+ '''
1449
+
1450
+ if not self.args['top']:
1451
+ return
1452
+
1453
+ if not (self.top_details and \
1454
+ self.top_details['inferred_from_target_name'] and \
1455
+ self.top_details['last_added_source_file']):
1456
+ return
1457
+
1458
+ # since work-dir should be set, we will change self.args['top'] if there
1459
+ # was a last-added source file.
1460
+ best_top_fname = self.top_details['last_added_source_file']
1461
+
1462
+ if not best_top_fname:
1463
+ return
1464
+
1465
+ best_top_name = self.get_top_name(best_top_fname)
1466
+ best_top = util.get_inferred_top_module_name(
1467
+ module_guess=best_top_name,
1468
+ module_fpath=best_top_fname
1469
+ )
1470
+ if not best_top:
1471
+ return
1472
+
1473
+ util.info("--top was previously inferred from target name",
1474
+ f"({self.top_details['inferred_from_target_name']}), overriding with:",
1475
+ f"{best_top} (from file: {best_top_fname})")
1476
+ self.args['top'] = best_top
1477
+
1356
1478
 
1357
1479
  def set_parameter(
1358
1480
  self, name: str, value, caller_info: str = '(CLI)',
@@ -1727,7 +1849,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1727
1849
 
1728
1850
  if not add_to_non_sources and \
1729
1851
  file_ext in known_file_ext_dict.get('inferred_top', []):
1730
- self.last_added_source_file_inferred_top = file_abspath
1852
+ self.top_details['last_added_source_file'] = file_abspath
1731
1853
 
1732
1854
  if add_to_non_sources:
1733
1855
  self.files_non_source.append(file_abspath)
@@ -1932,9 +2054,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1932
2054
  self.args['top'], top_path = last_potential_top_target
1933
2055
  util.info("--top not specified, inferred from target:",
1934
2056
  f"{self.args['top']} ({top_path})")
2057
+ self.top_details['inferred_from_target_name'] = top_path
1935
2058
 
1936
2059
  else:
1937
- best_top_fname = self.last_added_source_file_inferred_top
2060
+ best_top_fname = self.top_details['last_added_source_file']
1938
2061
  if best_top_fname:
1939
2062
  last_potential_top_file = (self.get_top_name(best_top_fname), best_top_fname)
1940
2063
 
@@ -1944,7 +2067,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1944
2067
  top_path = last_potential_top_file[1] # from tuple: (top, fpath)
1945
2068
  self.args['top'] = util.get_inferred_top_module_name(
1946
2069
  module_guess=last_potential_top_file[0],
1947
- module_fpath=last_potential_top_file[1]
2070
+ module_fpath=top_path
1948
2071
  )
1949
2072
  if self.args['top']:
1950
2073
  util.info("--top not specified, inferred from final source file:",
@@ -1953,6 +2076,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1953
2076
  # (not from DEPS.yml) we need to override self.target if that was set. Otherwise
1954
2077
  # it won't save to the correct work-dir:
1955
2078
  self.target = self.args['top']
2079
+ self.top_details['inferred_from_last_added_source_file'] = top_path
1956
2080
 
1957
2081
 
1958
2082
  util.info(f'{self.command_name}: top-most target name: {self.target}')
@@ -2174,7 +2298,10 @@ class CommandParallel(Command):
2174
2298
  if w.proc:
2175
2299
  util.info(f"need to KILL WORKER_{w.n}, probably needs manual cleanup, check 'ps'")
2176
2300
  if w.pid:
2177
- os.kill(w.pid, signal.SIGKILL)
2301
+ # Windows compatible: signal.SIGKILL is not available, so we could use
2302
+ # os.kill(PID, 9) but we'll be nice and do another SIGTERM
2303
+ # os.kill(PID, 9)
2304
+ os.kill(w.pid, getattr(signal, 'SIGKILL', signal.SIGTERM))
2178
2305
  util.stop_log()
2179
2306
  subprocess.Popen(['stty', 'sane']).wait() # pylint: disable=consider-using-with
2180
2307
 
opencos/eda_config.py CHANGED
@@ -10,11 +10,11 @@ Order of precedence for default value of eda arg: --config-yaml:
10
10
  import copy
11
11
  import os
12
12
  import argparse
13
- import shutil
14
13
 
15
14
  import mergedeep
16
15
 
17
16
  from opencos import util
17
+ from opencos.files import safe_shutil_which
18
18
  from opencos.util import safe_emoji
19
19
  from opencos.utils.markup_helpers import yaml_safe_load, yaml_safe_writer
20
20
 
@@ -22,14 +22,17 @@ class Defaults:
22
22
  '''Defaults is a global namespace for constants and supported features.
23
23
 
24
24
  Defaults.config_yml is set depending on search order for default eda_config[_defaults].yml
25
+
26
+ Note that opencos.eda and other packages are free to set and add additonal keys to the
27
+ "config", but the "supported_config_keys" are only what's allowed in the initial
28
+ --config-yml=YAML_FILE
25
29
  '''
26
30
 
27
- environ_override_config_yml = os.environ.get('EDA_CONFIG_YML', '')
28
- home_override_config_yml = os.path.join(
29
- os.environ.get('HOME', ''), '.opencos-eda', 'EDA_CONFIG.yml'
30
- )
31
+ environ_override_config_yml = ''
32
+ home_override_config_yml = ''
31
33
  opencos_config_yml = 'eda_config_defaults.yml'
32
34
  config_yml = ''
35
+ config_yml_set_from = ''
33
36
 
34
37
  supported_config_keys = set([
35
38
  'DEFAULT_HANDLERS', 'DEFAULT_HANDLERS_HELP',
@@ -39,21 +42,23 @@ class Defaults:
39
42
  'deps_markup_supported',
40
43
  'deps_subprocess_shell',
41
44
  'bare_plusarg_supported',
45
+ 'deps_expandvars_enable',
46
+ 'show_tool_versions',
42
47
  'dep_sub',
43
48
  'vars',
44
49
  'file_extensions',
45
50
  'command_determines_tool',
46
51
  'command_tool_is_optional',
52
+ 'command_has_subcommands',
47
53
  'tools',
48
54
  'auto_tools_order',
49
55
  ])
50
- supported_config_auto_tools_order_keys = set([
56
+ supported_config_tool_keys = set([
51
57
  'exe', 'handlers',
52
58
  'requires_env', 'requires_py', 'requires_cmd', 'requires_in_exe_path',
53
- 'requires_vsim_helper', 'requires_vscode_extension',
59
+ 'requires_vsim_helper',
60
+ 'requires_vscode_extension',
54
61
  'disable-tools-multi', 'disable-auto',
55
- ])
56
- supported_config_tool_keys = set([
57
62
  'defines',
58
63
  'log-bad-strings',
59
64
  'log-must-strings',
@@ -74,12 +79,36 @@ class Defaults:
74
79
 
75
80
  EDA_OUTPUT_CONFIG_FNAME = 'eda_output_config.yml'
76
81
 
77
- if os.path.isfile(Defaults.environ_override_config_yml):
78
- Defaults.config_yml = Defaults.environ_override_config_yml
79
- elif os.path.isfile(Defaults.home_override_config_yml):
80
- Defaults.config_yml = Defaults.home_override_config_yml
81
- else:
82
+ def set_defaults() -> None:
83
+ '''Updates Defaults *config_yml members, sets Defaults.config_yml'''
84
+
85
+ Defaults.environ_override_config_yml = os.environ.get(
86
+ 'EDA_CONFIG_YML', os.environ.get('EDA_CONFIG_YAML', '')
87
+ )
88
+ if Defaults.environ_override_config_yml and \
89
+ os.path.isfile(Defaults.environ_override_config_yml):
90
+ Defaults.config_yml = Defaults.environ_override_config_yml
91
+ Defaults.config_yml_set_from = 'env EDA_CONFIG_YML'
92
+ return
93
+
94
+ home = os.environ.get('HOME', os.environ.get('HOMEPATH', ''))
95
+ if home and os.path.isdir(os.path.join(home, '.opencos-eda')):
96
+ Defaults.home_override_config_yml = [
97
+ os.path.join(home, '.opencos-eda', 'EDA_CONFIG.yml'),
98
+ os.path.join(home, '.opencos-eda', 'EDA_CONFIG.yaml')
99
+ ]
100
+ for x in Defaults.home_override_config_yml:
101
+ if os.path.isfile(x):
102
+ Defaults.config_yml = x
103
+ Defaults.config_yml_set_from = 'file [$HOME|$HOMEPATH]/.opencos-eda'
104
+ return
105
+
106
+ # else default:
82
107
  Defaults.config_yml = Defaults.opencos_config_yml
108
+ Defaults.config_yml_set_from = ''
109
+
110
+
111
+ set_defaults()
83
112
 
84
113
 
85
114
  def find_eda_config_yml_fpath(
@@ -133,14 +162,6 @@ def check_config(config:dict, filename='') -> None:
133
162
  util.error(f'eda_config.get_config({filename=}): has unsupported {key=}' \
134
163
  + f' {Defaults.supported_config_keys=}')
135
164
 
136
- for row in config.get('auto_tools_order', []):
137
- for tool, table in row.items():
138
- for key in table:
139
- if key not in Defaults.supported_config_auto_tools_order_keys:
140
- util.error(f'eda_config.get_config({filename=}): has unsupported {key=}' \
141
- + f' in auto_tools_order, {tool=},' \
142
- + f' {Defaults.supported_config_auto_tools_order_keys=}')
143
-
144
165
  for tool,table in config.get('tools', {}).items():
145
166
  for key in table:
146
167
  if key not in Defaults.supported_config_tool_keys:
@@ -232,12 +253,21 @@ def get_config_merged_with_defaults(config:dict) -> dict:
232
253
 
233
254
  def get_argparser() -> argparse.ArgumentParser:
234
255
  '''Returns an ArgumentParser, handles --config-yml=<filename> arg'''
256
+
257
+ # re-run set_defaults() in case a --env-file overwrote Defaults.config_yml
258
+ set_defaults()
259
+
235
260
  parser = argparse.ArgumentParser(
236
261
  prog=f'{safe_emoji("🔎 ")}opencos eda config options', add_help=False, allow_abbrev=False
237
262
  )
238
- parser.add_argument('--config-yml', type=str, default=Defaults.config_yml,
239
- help=('YAML filename to use for configuration (default'
240
- f' {Defaults.config_yml})'))
263
+ parser.add_argument(
264
+ '--config-yml', type=str, default=Defaults.config_yml,
265
+ help=(
266
+ f'YAML filename to use for configuration (default {Defaults.config_yml}).'
267
+ ' Can be overriden using environmnet var EDA_CONFIG_YML=FILE, or from file'
268
+ ' [$HOME|$HOMEPATH]/.opencos-eda/EDA_CONFIG.yml'
269
+ )
270
+ )
241
271
  return parser
242
272
 
243
273
 
@@ -255,7 +285,6 @@ def get_eda_config(args:list, quiet=False) -> (dict, list):
255
285
 
256
286
  This will merge the result with the default config (if overriden)
257
287
  '''
258
-
259
288
  parser = get_argparser()
260
289
  try:
261
290
  parsed, unparsed = parser.parse_known_args(args + [''])
@@ -267,7 +296,13 @@ def get_eda_config(args:list, quiet=False) -> (dict, list):
267
296
 
268
297
  if parsed.config_yml:
269
298
  if not quiet:
270
- util.info(f'eda_config: --config-yml={parsed.config_yml} observed')
299
+ if parsed.config_yml != Defaults.config_yml:
300
+ # It was set on CLI:
301
+ util.info(f'eda_config: --config-yml={parsed.config_yml} observed')
302
+ elif Defaults.config_yml_set_from:
303
+ # It was picked up via env or HOME/.opencos-eda/ override:
304
+ util.info(f'eda_config: --config-yml={parsed.config_yml} observed, from',
305
+ f'{Defaults.config_yml_set_from}')
271
306
  fullpath = find_eda_config_yml_fpath(parsed.config_yml)
272
307
  config = get_config(fullpath)
273
308
  if not quiet:
@@ -354,10 +389,10 @@ def tool_try_add_to_path( # pylint: disable=too-many-branches
354
389
 
355
390
  name, path_arg = name_path_parts[0:2]
356
391
 
357
- if name not in config['auto_tools_order'][0]:
392
+ if name not in config['tools']:
358
393
  return name
359
394
 
360
- config_exe = config['auto_tools_order'][0][name].get('exe', str())
395
+ config_exe = config['tools'][name].get('exe', str())
361
396
  if isinstance(config_exe, list):
362
397
  orig_exe = config_exe[0]
363
398
  else:
@@ -390,21 +425,21 @@ def tool_try_add_to_path( # pylint: disable=too-many-branches
390
425
  util.error(f'--tool setting for {tool}: {user_exe} is not an executable')
391
426
  return name
392
427
 
393
- user_exe = shutil.which(user_exe)
428
+ user_exe = safe_shutil_which(user_exe)
394
429
 
395
430
  if update_config:
396
431
  if isinstance(config_exe, list):
397
- config['auto_tools_order'][0][name]['exe'][0] = user_exe
432
+ config['tools'][name]['exe'][0] = user_exe
398
433
  for index,value in enumerate(config_exe[1:]):
399
434
  # update all entries, if we can, if the value is also in 'path'
400
435
  # from our set --tool=Name=path/exe
401
436
  new_value = os.path.join(path, os.path.split(value)[1])
402
- if os.path.exists(new_value) and shutil.which(new_value) and \
437
+ if os.path.exists(new_value) and safe_shutil_which(new_value) and \
403
438
  os.access(new_value, os.X_OK):
404
- config['auto_tools_order'][0][name]['exe'][index] = new_value
439
+ config['tools'][name]['exe'][index] = new_value
405
440
  else:
406
- config['auto_tools_order'][0][name]['exe'] = user_exe
407
- util.debug(f'For {tool=}, auto_tools_order config updated')
441
+ config['tools'][name]['exe'] = user_exe
442
+ util.debug(f'For {tool=}, tools config entry updated')
408
443
 
409
444
  util.debug(f'For {tool=}, final {user_exe=}')
410
445