opencos-eda 0.3.10__py3-none-any.whl → 0.3.11__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 (53) hide show
  1. opencos/commands/deps_help.py +63 -113
  2. opencos/commands/export.py +7 -2
  3. opencos/commands/multi.py +3 -3
  4. opencos/commands/sim.py +14 -15
  5. opencos/commands/synth.py +1 -2
  6. opencos/commands/upload.py +192 -4
  7. opencos/commands/waves.py +8 -8
  8. opencos/deps/deps_commands.py +6 -6
  9. opencos/deps/deps_processor.py +129 -50
  10. opencos/docs/Architecture.md +45 -0
  11. opencos/docs/ConnectingApps.md +29 -0
  12. opencos/docs/DEPS.md +199 -0
  13. opencos/docs/Debug.md +77 -0
  14. opencos/docs/DirectoryStructure.md +22 -0
  15. opencos/docs/Installation.md +117 -0
  16. opencos/docs/OcVivadoTcl.md +63 -0
  17. opencos/docs/OpenQuestions.md +7 -0
  18. opencos/docs/README.md +13 -0
  19. opencos/docs/RtlCodingStyle.md +54 -0
  20. opencos/docs/eda.md +147 -0
  21. opencos/docs/oc_cli.md +135 -0
  22. opencos/eda.py +132 -35
  23. opencos/eda_base.py +173 -47
  24. opencos/eda_config.py +56 -17
  25. opencos/eda_config_defaults.yml +21 -4
  26. opencos/files.py +26 -1
  27. opencos/tools/cocotb.py +5 -5
  28. opencos/tools/invio.py +2 -2
  29. opencos/tools/invio_yosys.py +2 -1
  30. opencos/tools/iverilog.py +3 -3
  31. opencos/tools/quartus.py +113 -115
  32. opencos/tools/questa_common.py +2 -2
  33. opencos/tools/riviera.py +3 -3
  34. opencos/tools/slang.py +11 -7
  35. opencos/tools/slang_yosys.py +1 -0
  36. opencos/tools/surelog.py +4 -3
  37. opencos/tools/verilator.py +4 -4
  38. opencos/tools/vivado.py +307 -176
  39. opencos/tools/yosys.py +4 -4
  40. opencos/util.py +6 -3
  41. opencos/utils/dict_helpers.py +31 -0
  42. opencos/utils/markup_helpers.py +2 -2
  43. opencos/utils/subprocess_helpers.py +3 -3
  44. opencos/utils/vscode_helper.py +2 -2
  45. opencos/utils/vsim_helper.py +16 -5
  46. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.11.dist-info}/METADATA +1 -1
  47. opencos_eda-0.3.11.dist-info/RECORD +94 -0
  48. opencos_eda-0.3.10.dist-info/RECORD +0 -81
  49. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.11.dist-info}/WHEEL +0 -0
  50. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.11.dist-info}/entry_points.txt +0 -0
  51. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.11.dist-info}/licenses/LICENSE +0 -0
  52. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.11.dist-info}/licenses/LICENSE.spdx +0 -0
  53. {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.11.dist-info}/top_level.txt +0 -0
opencos/eda.py CHANGED
@@ -9,7 +9,6 @@ markup files
9
9
  import subprocess
10
10
  import os
11
11
  import sys
12
- import shutil
13
12
  import re
14
13
  import signal
15
14
  import argparse
@@ -20,10 +19,10 @@ from pathlib import Path
20
19
 
21
20
  import opencos
22
21
  from opencos import util, eda_config, eda_base
22
+ from opencos.eda_base import Command, Tool, which_tool, print_eda_usage_line
23
+ from opencos.files import safe_shutil_which
23
24
  from opencos.util import safe_emoji, Colors
24
- from opencos.eda_base import Tool, which_tool, get_eda_exec, print_eda_usage_line
25
25
  from opencos.utils import vsim_helper, vscode_helper
26
- from opencos.utils.subprocess_helpers import subprocess_run_background
27
26
  from opencos.utils import status_constants, str_helpers, subprocess_helpers
28
27
 
29
28
  # Configure util:
@@ -44,7 +43,8 @@ util.global_log.default_log_disable_with_args.extend([
44
43
  # These are also overriden depending on the tool, for example --tool verilator sets
45
44
  # "sim": CommandSimVerilator.
46
45
  def init_config(
47
- config: dict, quiet: bool = False, tool=None, run_auto_tool_setup: bool = True
46
+ config: dict, quiet: bool = False, tool=None, command: str = '',
47
+ run_auto_tool_setup: bool = True
48
48
  ) -> dict:
49
49
  '''Sets or clears entries in config (dict) so tools can be re-loaded.'''
50
50
 
@@ -64,7 +64,7 @@ def init_config(
64
64
  config['auto_tools_found'] = {}
65
65
  config['tools_loaded'] = set()
66
66
  if run_auto_tool_setup:
67
- config = auto_tool_setup(config=config, quiet=quiet, tool=tool)
67
+ config = auto_tool_setup(config=config, quiet=quiet, tool=tool, command=command)
68
68
  return config
69
69
 
70
70
 
@@ -137,7 +137,11 @@ def interactive(config: dict) -> int:
137
137
  '''
138
138
  rc = 0
139
139
  while True:
140
- line = input('EDA->')
140
+ try:
141
+ line = input('EDA->')
142
+ except EOFError:
143
+ util.info('End of input reached unexpectedly, exiting')
144
+ return 0
141
145
  m = re.match(r'^([^\#]*)\#.*$', line)
142
146
  if m:
143
147
  line = m.group(1)
@@ -154,7 +158,8 @@ def interactive(config: dict) -> int:
154
158
 
155
159
 
156
160
  def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
157
- warnings: bool = True, config = None, quiet: bool = False, tool: str = ''
161
+ warnings: bool = True, config = None, quiet: bool = False, tool: str = '',
162
+ command: str = ''
158
163
  ) -> dict:
159
164
  '''Returns an updated config, uses config['auto_tools_order'][0] dict, calls tool_setup(..)
160
165
 
@@ -172,9 +177,26 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
172
177
  assert isinstance(config['auto_tools_order'], list)
173
178
  assert isinstance(config['auto_tools_order'][0], dict)
174
179
 
180
+ if command:
181
+ util.info(f'Auto tool setup for command: {Colors.byellow}{command}')
182
+
175
183
  for name, value in config['auto_tools_order'][0].items():
176
184
  if tool and tool != name:
177
- continue # if called with tool=(some_name), then only load that tool.
185
+ # if called with tool=(some_name), then only load that tool (which is not
186
+ # this one)
187
+ continue
188
+
189
+ if command and command not in value.get('handlers', {}) and \
190
+ command not in config.get('command_has_subcommands', []):
191
+ # Skip tool_setup(..) if the tool handlers can't support command,
192
+ # this is a time-saving feature, but if the comman is multi, tools-multi,
193
+ # sweep, then don't skip this (we don't know what tools we need so load them
194
+ # all.
195
+ # We could figure this out if we went looking for all command(s)
196
+ # multi + sub-command, but that's slightly dangerous if we grab a 'command'
197
+ # from another arg.
198
+ util.debug(f"Skipping tool {name} because it cannot handle {command=}")
199
+ continue
178
200
 
179
201
  util.debug(f"Checking for ability to run tool: {name}")
180
202
  exe = value.get('exe', str())
@@ -207,7 +229,7 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
207
229
  exe_path = None
208
230
  for exe in exe_list:
209
231
  assert exe != '', f'{name=} {value=} value missing "exe" {exe=}'
210
- p = shutil.which(exe)
232
+ p = safe_shutil_which(exe)
211
233
  if not exe_path:
212
234
  exe_path = p # set on first required exe
213
235
  if not p:
@@ -261,18 +283,22 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
261
283
  if exe_path:
262
284
  p = exe_path
263
285
  else:
264
- p = shutil.which(exe_list[0])
286
+ p = safe_shutil_which(exe_list[0])
265
287
  config['auto_tools_found'][name] = p # populate key-value pairs w/ first exe in list
266
288
  if not quiet:
267
289
  util.info(f"Detected {name} ({p})")
268
290
  tool_setup(
269
- tool=name, quiet=True, auto_setup=True, warnings=warnings, config=config
291
+ tool=name, quiet=True, auto_setup=(not tool), warnings=warnings, config=config
270
292
  )
293
+ else:
294
+ util.debug(f'Tool {name} is missing one of: {has_all_py=} {has_all_env=}',
295
+ f'{has_all_exe=} {has_all_in_exe_path=} {has_vsim_helper=}',
296
+ f'{has_vscode_helper=}')
271
297
 
272
298
  return config
273
299
 
274
300
 
275
- def tool_setup(
301
+ def tool_setup( # pylint: disable=too-many-branches
276
302
  tool: str, config: dict, quiet: bool = False, auto_setup: bool = False,
277
303
  warnings: bool = True
278
304
  ):
@@ -332,10 +358,22 @@ def tool_setup(
332
358
 
333
359
  cls = util.import_class_from_string(str_class_name)
334
360
 
335
- assert issubclass(cls, Tool), \
336
- f'{str_class_name=} is does not have Tool class associated with it'
337
- util.debug(f'Setting {cls=} for {command=} in config.command_handler')
338
- config['command_handler'][command] = cls
361
+ if command in config.get('command_determines_tool', []) + \
362
+ config.get('command_tool_is_optional', []):
363
+ # we don't need to confirm the handler parent is a Tool class.
364
+ pass
365
+ else:
366
+ assert issubclass(cls, Tool), \
367
+ f'{str_class_name=} is does not have Tool class associated with it'
368
+
369
+ if not auto_setup or \
370
+ command not in config.get('command_determines_tool', []):
371
+ # If not auto_setup - then someone called this --tool by name on the command line,
372
+ # then update the command handler
373
+ # otherwise, if --tool was not set, and command determines tool, leave it with
374
+ # the default handler.
375
+ util.debug(f'Setting {cls=} for {command=} in config.command_handler')
376
+ config['command_handler'][command] = cls
339
377
 
340
378
  config['tools_loaded'].add(tool)
341
379
 
@@ -358,7 +396,6 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
358
396
  deferred_tokens = []
359
397
  command = ""
360
398
  run_auto_tool_setup = True
361
- process_tokens_cwd = os.getcwd()
362
399
 
363
400
  parser = eda_base.get_argparser()
364
401
  try:
@@ -385,6 +422,8 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
385
422
 
386
423
  # Attempt to get the 'command' in the unparsed args before we've even
387
424
  # set the command handlers (some commands don't use tools).
425
+ # Note that we only grab the first command, and for multi, tools-multi,
426
+ # or sweep we do NOT get the subcommand.
388
427
  for value in unparsed:
389
428
  if value in config['DEFAULT_HANDLERS'].keys():
390
429
  command = value
@@ -400,7 +439,7 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
400
439
  # This will handle any --tool=<name>=/path/to/bin also, so don't have to
401
440
  # run tool_setup(..) on its own.
402
441
  config = init_config(
403
- config, tool=parsed.tool,
442
+ config, tool=parsed.tool, command=command,
404
443
  run_auto_tool_setup=run_auto_tool_setup
405
444
  )
406
445
  if not config:
@@ -467,26 +506,82 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
467
506
  util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
468
507
 
469
508
  if rc == 0 and not parsed.tool and getattr(sco, 'tool_changed_respawn', False):
470
- use_tool = sco.args.get('tool', '')
471
- if not use_tool:
472
- util.error(f'Unable to change tool from {parsed.tool}, internal eda problem.')
473
- return status_constants.EDA_DEFAULT_ERROR
474
-
475
- # close the util.log:
476
- util.stop_log()
477
- # respawn the original job, but with --tool=<use_tool> applied:
478
- _command_list = [get_eda_exec(command), f"--tool={use_tool}"] + original_args
479
- util.info(f'eda: respawn for tool change: {" ".join(_command_list)};',
480
- f' (running from: {process_tokens_cwd})')
481
- subprocess_run_background(
482
- work_dir=process_tokens_cwd,
483
- command_list=_command_list,
484
- background=util.args.get('quiet', False)
509
+ return respawn_new_sub_command_object(
510
+ sco=sco, parsed=parsed, config=config, command=command, tokens=tokens,
511
+ deferred_tokens=deferred_tokens
485
512
  )
486
513
 
487
514
  return rc
488
515
 
489
516
 
517
+ def respawn_new_sub_command_object(
518
+ sco: Command, parsed: argparse.Namespace, config: dict, command: str,
519
+ tokens: list, deferred_tokens: list
520
+ ) -> int:
521
+ '''Returns retcode (int). Creates a new Command object, presumably using a different tool,
522
+
523
+ due to args changes from DEPS parsing that led to --tool=<different value> vs the automatic
524
+ value if --tool was not originally set. Will run process_tokens(..) on the new sub commmand
525
+ object.
526
+ '''
527
+
528
+ use_tool = sco.args.get('tool', '')
529
+
530
+ if not use_tool:
531
+ util.error(f'Unable to change tool from {parsed.tool}, internal eda problem.')
532
+ return status_constants.EDA_DEFAULT_ERROR
533
+
534
+ util.info(f'Changing {Colors.bcyan}--tool{Colors.normal}{Colors.green} --->',
535
+ f'{Colors.bcyan}{use_tool}{Colors.normal}{Colors.green} for command:',
536
+ f'{Colors.byellow}{command}')
537
+
538
+ # Update the command handler(s) with this new tool. We don't really respawn, just
539
+ # try to swap out the sco (Command obj handle)
540
+ entry = config['auto_tools_order'][0].get(use_tool, {})
541
+ tool_cmd_handler_dict = entry.get('handlers', {})
542
+
543
+ for _command, str_class_name in tool_cmd_handler_dict.items():
544
+ if _command and command and _command != command:
545
+ # This isn't the command we care about (it's just one of the commands
546
+ # this tool supports, so don't bother loading a handler for it:
547
+ continue
548
+
549
+ cls = util.import_class_from_string(str_class_name)
550
+ if _command in config.get('command_determines_tool', []) + \
551
+ config.get('command_tool_is_optional', []):
552
+ # we don't need to confirm the handler parent is a Tool class.
553
+ pass
554
+ else:
555
+ assert issubclass(cls, Tool), \
556
+ f'command {_command} {str_class_name=} does not have Tool class associated with it'
557
+
558
+ util.debug(f'Setting {cls=} for command={_command} in config.command_handler')
559
+ config['command_handler'][_command] = cls
560
+
561
+ old_sco = sco
562
+ sco = config['command_handler'][command](config=config) # sub command object
563
+ util.debug(f'No longer using handler: {type(old_sco)}; now using: {type(sco)}')
564
+ sco.config['eda_original_args'] = old_sco.config['eda_original_args']
565
+ del old_sco
566
+
567
+ rc = check_command_handler_cls(command_obj=sco, command=command, parsed_args=parsed)
568
+ if rc > 0:
569
+ util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=},'
570
+ f'unparsed={deferred_tokens}')
571
+ return rc
572
+
573
+ setattr(sco, 'command_name', command) # as a safeguard, 'command' set in 'sco'
574
+ util.info(f'--tool={use_tool}: running command: {Colors.byellow}eda {command} ',
575
+ ' '.join(deferred_tokens))
576
+ unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
577
+
578
+ # query the status from the Command object (0 is pass, > 0 is fail, but we'd prefer to
579
+ # avoid rc=1 because that's the python exception rc)
580
+ rc = getattr(sco, 'status', 2)
581
+ util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
582
+ return rc
583
+
584
+
490
585
  def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> int:
491
586
  '''Returns bash/sh return code, checks that a command handling class has all
492
587
 
@@ -524,7 +619,7 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
524
619
  if k == 'exe' or k.startswith('requires_cmd'):
525
620
  util.warning(
526
621
  f"tool '{parsed_tool}' has requirements that may not have been met --",
527
- f"{k}: {v}"
622
+ f"{k}: {v} (Perhaps not in PATH?)"
528
623
  )
529
624
  if k == 'requires_vsim_helper':
530
625
  if found_tool := vsim_helper.found():
@@ -595,7 +690,8 @@ def main(*args):
595
690
  if not util.args['quiet']:
596
691
  util.info(f'eda: version {opencos.__version__}', color=Colors.bcyan)
597
692
  # And show the command that was run (all args):
598
- util.info(f"main: eda {' '.join(args)}; (run from {os.getcwd()})")
693
+ util.info(f"main: {Colors.byellow}eda {' '.join(args)}{Colors.normal}{Colors.green};",
694
+ f"(run from {os.getcwd()})")
599
695
 
600
696
  # Handle --config-yml= arg
601
697
  config, unparsed = eda_config.get_eda_config(unparsed)
@@ -657,6 +753,7 @@ def main_cli() -> None:
657
753
  util.global_exit_allowed = True
658
754
  # Strip eda or eda.py from sys.argv, we know who we are if called from __main__:
659
755
  rc = main()
756
+ subprocess_helpers.cleanup_all()
660
757
  util.exit(rc)
661
758
 
662
759
 
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()
@@ -214,7 +215,7 @@ class Tool:
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
@@ -240,14 +241,18 @@ class Tool:
240
241
  return
241
242
 
242
243
  info_color = Colors.green
244
+ error_color = Colors.bgreen
243
245
  start = ''
244
- if self.tool_error_count or self.tool_warning_count:
245
- start = safe_emoji('🔶 ')
246
+ if self.tool_warning_count:
247
+ info_color = Colors.yellow
248
+ if self.tool_error_count:
246
249
  info_color = Colors.yellow
250
+ error_color = Colors.byellow
251
+ start = safe_emoji('🔶 ')
247
252
  util.info(
248
- f"{start}Tool - {tool_name}, total counts:",
253
+ f"{start}Tool - {Colors.bold}{tool_name}{Colors.normal}{info_color}, total counts:",
249
254
  f"{Colors.bold}{self.tool_warning_count} tool warnings{Colors.normal}{info_color},",
250
- f"{Colors.bold}{self.tool_error_count} tool errors",
255
+ f"{error_color}{self.tool_error_count} tool errors",
251
256
  color=info_color
252
257
  )
253
258
 
@@ -272,6 +277,8 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
272
277
  self.args_help = {}
273
278
  if getattr(self, 'args_kwargs', None) is None:
274
279
  self.args_kwargs = {}
280
+ if getattr(self, 'args_args', None) is None:
281
+ self.args_args = {} # support for multiple named --arg that map to same self.args key.
275
282
  self.args.update({
276
283
  "keep" : False,
277
284
  "force" : False,
@@ -358,6 +365,7 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
358
365
  self.errors_log_f = None
359
366
  self.auto_tool_applied = False
360
367
  self.tool_changed_respawn = {}
368
+ self.top_details = {}
361
369
 
362
370
 
363
371
  def get_info_job_name(self) -> str:
@@ -558,8 +566,23 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
558
566
  # Set the util.artifacts path with our work-dir:
559
567
  util.artifacts.set_artifacts_json_dir(self.args['work-dir'])
560
568
 
569
+ # Since work-dir has now safely been created (or error flagged) we can override the
570
+ # inferred 'top' if top was not specified (aka, we guessed based on target name or
571
+ # file/module name)
572
+ self.update_top_if_inferred()
573
+
561
574
  return self.args['work-dir']
562
575
 
576
+
577
+ def update_top_if_inferred(self) -> None:
578
+ '''Child classes can override (CommandDesign does)
579
+
580
+ Idea is to overwrite self.args['top'] if it was inferred, but Command.args
581
+ does not initially set self.args['top'], so this method takes no action.
582
+ '''
583
+ return
584
+
585
+
563
586
  def artifacts_add(self, name: str, typ: str, description: str) -> None:
564
587
  '''Adds a file to util.artifacts, derived classes may override'''
565
588
  util.artifacts.add(name=name, typ=typ, description=description)
@@ -710,6 +733,9 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
710
733
  arguments = [] # list supplied to parser.add_argument(..) so one liner supports both.
711
734
  for this_key in keys:
712
735
  arguments.append(f'--{this_key}')
736
+ for arg in self.args_args.get(this_key, []):
737
+ if arg not in arguments:
738
+ arguments.append(f'--{arg}')
713
739
 
714
740
  if self.args_help.get(key, ''):
715
741
  help_kwargs = {'help': self.args_help[key] + f' (default={value})'}
@@ -838,6 +864,9 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
838
864
  '''
839
865
 
840
866
  _, unparsed = self.run_argparser_on_list(tokens)
867
+
868
+ self._apply_deps_file_defaults_if_present()
869
+
841
870
  if process_all and unparsed:
842
871
  self.warning_show_known_args()
843
872
  self.error_ifarg(
@@ -983,37 +1012,39 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
983
1012
  color.disable() # strip our color object if < 3.14
984
1013
 
985
1014
 
1015
+ # Include -G, +incdir+, +define+ help if this is a CommanDesign class:
986
1016
  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('')
1017
+ if isinstance(self, CommandDesign):
1018
+ lines.append(
1019
+ f" {color.cyan}-G{color.byellow}<parameterName>{color.normal}=" \
1020
+ + f"{color.yellow}<value>{color.normal}"
1021
+ )
1022
+ lines.append(indent_me((
1023
+ " Add parameter to top level, support bit/int/string types only."
1024
+ " Example: -GDEPTH=8 (DEPTH treated as SV int/integer)."
1025
+ " -GENABLE=1 (ENABLED treated as SV bit/int/integer)."
1026
+ " -GName=eda (Name treated as SV string \"eda\")."
1027
+ )))
1028
+
1029
+ lines.append(f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}")
1030
+ lines.append(indent_me((
1031
+ " Add define w/out value to tool ahead of SV sources"
1032
+ " Example: +define+SIM_SPEEDUP"
1033
+ )))
1034
+ lines.append(
1035
+ f" {color.cyan}+define+{color.byellow}<defineName>{color.normal}=" \
1036
+ + f"{color.yellow}<value>{color.normal}")
1037
+ lines.append(indent_me((
1038
+ " Add define w/ value to tool ahead of SV sources"
1039
+ " Example: +define+TECH_LIB=2 +define+FULL_NAME=\"E D A\""
1040
+ )))
1041
+ lines.append(f" {color.cyan}+incdir+{color.byellow}PATH{color.normal}")
1042
+ lines.append(indent_me((
1043
+ " Add path (absolute or relative) for include directories"
1044
+ " for SystemVerilog `include \"<some-file>.svh\""
1045
+ " Example: +incdir+../lib"
1046
+ )))
1047
+ lines.append('')
1017
1048
 
1018
1049
  for line in lines:
1019
1050
  print(line)
@@ -1082,6 +1113,13 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
1082
1113
  else:
1083
1114
  util.warning(*msg)
1084
1115
 
1116
+ @staticmethod
1117
+ def get_top_name(name: str) -> str:
1118
+ '''Attempt to get the 'top' module name from a file, such as path/to/mine.sv will
1119
+
1120
+ return "mine"'''
1121
+ return os.path.splitext(os.path.basename(name))[0]
1122
+
1085
1123
 
1086
1124
  def update_tool_warn_err_counts_from_log_lines(
1087
1125
  self, log_lines: list, bad_strings: list, warning_strings: list
@@ -1103,6 +1141,48 @@ class Command: # pylint: disable=too-many-public-methods,too-many-instance-attri
1103
1141
  setattr(self, 'tool_warning_count', getattr(self, 'tool_warning_count', 0) + 1)
1104
1142
 
1105
1143
 
1144
+ def _apply_deps_file_defaults_if_present(self) -> None:
1145
+ '''Only runs if this is not a CommandDesign class.
1146
+
1147
+ Looks at DEPS file in current directory, and will attempt to apply 'DEFAULTS' if present
1148
+ '''
1149
+
1150
+ if isinstance(self, CommandDesign):
1151
+ return
1152
+
1153
+ # Try to fish out DEFAULTS from ./DEPS if present, so we can apply args to self.
1154
+ _cache_none = {}
1155
+ data = None
1156
+ if self.config['deps_markup_supported']:
1157
+ deps = DepsFile(
1158
+ command_design_ref=self, target_path=str(Path('.')), cache=_cache_none
1159
+ )
1160
+ data = deps.data
1161
+
1162
+ entry = None
1163
+ if data is not None and 'DEFAULTS' in data:
1164
+ entry = deps.get_entry(target_node=str(Path('./DEFAULTS')))
1165
+ if not entry:
1166
+ return
1167
+
1168
+ target = str(Path('./DEFAULTS'))
1169
+ _, target_node = os.path.split(target)
1170
+ caller_info = deps.gen_caller_info(target)
1171
+
1172
+ deps_processor = DepsProcessor(
1173
+ command_design_ref=self,
1174
+ deps_entry=entry,
1175
+ target=target,
1176
+ target_path=str(Path('.')),
1177
+ target_node=target_node,
1178
+ deps_file = deps.deps_file,
1179
+ caller_info = caller_info
1180
+ )
1181
+ _ = deps_processor.process_deps_entry()
1182
+
1183
+
1184
+
1185
+
1106
1186
  class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1107
1187
  '''CommandDesign is the eda base class for command handlers that need to track files.
1108
1188
 
@@ -1161,11 +1241,22 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1161
1241
  # keys 'data' and 'line_numbers'
1162
1242
  self.cached_deps = {}
1163
1243
  self.targets_dict = {} # key = targets that we've already processed in DEPS files
1164
- self.last_added_source_file_inferred_top = ''
1165
1244
 
1166
1245
  self.has_pre_compile_dep_shell_commands = False
1167
1246
  self.has_post_tool_dep_shell_commands = False
1168
1247
 
1248
+ self.top_details = {
1249
+ # used by CommandDesign to track last file added.
1250
+ 'last_added_source_file': '',
1251
+ # implies top was not set, we inferred from the final command line
1252
+ # DEPS target name
1253
+ 'inferred_from_target_name': False,
1254
+ # implies top was not set, we inferred from the final source file added,
1255
+ # based on a 'module' name we parsed.
1256
+ # also implies self.target was set from inferred top:
1257
+ 'inferred_from_last_added_source_file': False,
1258
+ }
1259
+
1169
1260
 
1170
1261
  def run_dep_commands(self) -> None:
1171
1262
  '''Run shell/peakrdl style commands from DEPS files, this is peformed before
@@ -1346,13 +1437,43 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1346
1437
  else:
1347
1438
  os.symlink(src=fname, dst=destfile)
1348
1439
 
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
1440
 
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]
1441
+ def update_top_if_inferred(self) -> None:
1442
+ '''Overridden from Command, uses self.top_details - attempts to overwrite
1443
+
1444
+ self.args['top'] if it was previously set based on the first DEPS target name,
1445
+ so it more accurately reflects the top-module-name. You should only do this if the
1446
+ self.args['work-dir'] has already been determined.
1447
+ '''
1448
+
1449
+ if not self.args['top']:
1450
+ return
1451
+
1452
+ if not (self.top_details and \
1453
+ self.top_details['inferred_from_target_name'] and \
1454
+ self.top_details['last_added_source_file']):
1455
+ return
1456
+
1457
+ # since work-dir should be set, we will change self.args['top'] if there
1458
+ # was a last-added source file.
1459
+ best_top_fname = self.top_details['last_added_source_file']
1460
+
1461
+ if not best_top_fname:
1462
+ return
1463
+
1464
+ best_top_name = self.get_top_name(best_top_fname)
1465
+ best_top = util.get_inferred_top_module_name(
1466
+ module_guess=best_top_name,
1467
+ module_fpath=best_top_fname
1468
+ )
1469
+ if not best_top:
1470
+ return
1471
+
1472
+ util.info("--top was previously inferred from target name",
1473
+ f"({self.top_details['inferred_from_target_name']}), overriding with:",
1474
+ f"{best_top} (from file: {best_top_fname})")
1475
+ self.args['top'] = best_top
1476
+
1356
1477
 
1357
1478
  def set_parameter(
1358
1479
  self, name: str, value, caller_info: str = '(CLI)',
@@ -1727,7 +1848,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1727
1848
 
1728
1849
  if not add_to_non_sources and \
1729
1850
  file_ext in known_file_ext_dict.get('inferred_top', []):
1730
- self.last_added_source_file_inferred_top = file_abspath
1851
+ self.top_details['last_added_source_file'] = file_abspath
1731
1852
 
1732
1853
  if add_to_non_sources:
1733
1854
  self.files_non_source.append(file_abspath)
@@ -1932,9 +2053,10 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1932
2053
  self.args['top'], top_path = last_potential_top_target
1933
2054
  util.info("--top not specified, inferred from target:",
1934
2055
  f"{self.args['top']} ({top_path})")
2056
+ self.top_details['inferred_from_target_name'] = top_path
1935
2057
 
1936
2058
  else:
1937
- best_top_fname = self.last_added_source_file_inferred_top
2059
+ best_top_fname = self.top_details['last_added_source_file']
1938
2060
  if best_top_fname:
1939
2061
  last_potential_top_file = (self.get_top_name(best_top_fname), best_top_fname)
1940
2062
 
@@ -1944,7 +2066,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1944
2066
  top_path = last_potential_top_file[1] # from tuple: (top, fpath)
1945
2067
  self.args['top'] = util.get_inferred_top_module_name(
1946
2068
  module_guess=last_potential_top_file[0],
1947
- module_fpath=last_potential_top_file[1]
2069
+ module_fpath=top_path
1948
2070
  )
1949
2071
  if self.args['top']:
1950
2072
  util.info("--top not specified, inferred from final source file:",
@@ -1953,6 +2075,7 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1953
2075
  # (not from DEPS.yml) we need to override self.target if that was set. Otherwise
1954
2076
  # it won't save to the correct work-dir:
1955
2077
  self.target = self.args['top']
2078
+ self.top_details['inferred_from_last_added_source_file'] = top_path
1956
2079
 
1957
2080
 
1958
2081
  util.info(f'{self.command_name}: top-most target name: {self.target}')
@@ -2174,7 +2297,10 @@ class CommandParallel(Command):
2174
2297
  if w.proc:
2175
2298
  util.info(f"need to KILL WORKER_{w.n}, probably needs manual cleanup, check 'ps'")
2176
2299
  if w.pid:
2177
- os.kill(w.pid, signal.SIGKILL)
2300
+ # Windows compatible: signal.SIGKILL is not available, so we could use
2301
+ # os.kill(PID, 9) but we'll be nice and do another SIGTERM
2302
+ # os.kill(PID, 9)
2303
+ os.kill(w.pid, getattr(signal, 'SIGKILL', signal.SIGTERM))
2178
2304
  util.stop_log()
2179
2305
  subprocess.Popen(['stty', 'sane']).wait() # pylint: disable=consider-using-with
2180
2306