opencos-eda 0.3.9__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 (74) hide show
  1. opencos/commands/deps_help.py +89 -120
  2. opencos/commands/export.py +7 -2
  3. opencos/commands/multi.py +3 -3
  4. opencos/commands/sim.py +14 -16
  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_file.py +82 -79
  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 +175 -41
  24. opencos/eda_base.py +180 -50
  25. opencos/eda_config.py +62 -16
  26. opencos/eda_config_defaults.yml +21 -4
  27. opencos/eda_deps_bash_completion.bash +37 -15
  28. opencos/files.py +26 -1
  29. opencos/tools/cocotb.py +5 -5
  30. opencos/tools/invio.py +2 -2
  31. opencos/tools/invio_yosys.py +2 -1
  32. opencos/tools/iverilog.py +3 -3
  33. opencos/tools/quartus.py +113 -115
  34. opencos/tools/questa_common.py +3 -4
  35. opencos/tools/riviera.py +3 -3
  36. opencos/tools/slang.py +11 -7
  37. opencos/tools/slang_yosys.py +1 -0
  38. opencos/tools/surelog.py +4 -3
  39. opencos/tools/verilator.py +5 -4
  40. opencos/tools/vivado.py +307 -176
  41. opencos/tools/yosys.py +4 -4
  42. opencos/util.py +6 -3
  43. opencos/utils/dict_helpers.py +31 -0
  44. opencos/utils/markup_helpers.py +2 -2
  45. opencos/utils/str_helpers.py +7 -0
  46. opencos/utils/subprocess_helpers.py +3 -3
  47. opencos/utils/vscode_helper.py +2 -2
  48. opencos/utils/vsim_helper.py +58 -22
  49. {opencos_eda-0.3.9.dist-info → opencos_eda-0.3.11.dist-info}/METADATA +1 -1
  50. opencos_eda-0.3.11.dist-info/RECORD +94 -0
  51. {opencos_eda-0.3.9.dist-info → opencos_eda-0.3.11.dist-info}/entry_points.txt +1 -0
  52. opencos/tests/__init__.py +0 -0
  53. opencos/tests/custom_config.yml +0 -13
  54. opencos/tests/deps_files/command_order/DEPS.yml +0 -44
  55. opencos/tests/deps_files/error_msgs/DEPS.yml +0 -55
  56. opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -4
  57. opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  58. opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -50
  59. opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -54
  60. opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -4
  61. opencos/tests/helpers.py +0 -354
  62. opencos/tests/test_build.py +0 -12
  63. opencos/tests/test_deps_helpers.py +0 -207
  64. opencos/tests/test_deps_schema.py +0 -30
  65. opencos/tests/test_eda.py +0 -921
  66. opencos/tests/test_eda_elab.py +0 -110
  67. opencos/tests/test_eda_synth.py +0 -150
  68. opencos/tests/test_oc_cli.py +0 -25
  69. opencos/tests/test_tools.py +0 -404
  70. opencos_eda-0.3.9.dist-info/RECORD +0 -99
  71. {opencos_eda-0.3.9.dist-info → opencos_eda-0.3.11.dist-info}/WHEEL +0 -0
  72. {opencos_eda-0.3.9.dist-info → opencos_eda-0.3.11.dist-info}/licenses/LICENSE +0 -0
  73. {opencos_eda-0.3.9.dist-info → opencos_eda-0.3.11.dist-info}/licenses/LICENSE.spdx +0 -0
  74. {opencos_eda-0.3.9.dist-info → opencos_eda-0.3.11.dist-info}/top_level.txt +0 -0
opencos/docs/eda.md ADDED
@@ -0,0 +1,147 @@
1
+ # EDA Tool
2
+
3
+ ## Install
4
+
5
+ Using `uv` (recommended):
6
+
7
+ From PyPI:
8
+ ```
9
+ uv tool install opencos-eda
10
+ ```
11
+
12
+ OR, from the repo:
13
+ ```
14
+ uv tool install /path/to/your/checkout/dir/of/opencos
15
+ ```
16
+
17
+ This makes the `eda` and `oc_cli` commands available in your environment.
18
+
19
+ ## Basic Recipes
20
+
21
+ Run a single simulation using the default simulator (of those installed).
22
+ `oclib_fifo_test` is target in `./lib/tests/DEPS.yml`
23
+ ```
24
+ eda sim lib/tests/oclib_fifo_test
25
+ ```
26
+
27
+ Run the same single simulation, but use `verilator`, dump waves
28
+ ```
29
+ eda sim --waves --tool verilator lib/tests/oclib_fifo_test
30
+ ```
31
+
32
+ Run the same single simulation, but use `vivado` XSim, and run in GUI mode
33
+ ```
34
+ eda sim --gui --tool vivado lib/tests/oclib_fifo_test
35
+ ```
36
+
37
+ Run a compile + elab without a DEPS.yml file or target involved for dependencies
38
+ ```
39
+ eda elab --tool verilator +incdir+. ./lib/oclib_assert_pkg.sv ./lib/oclib_simple_reset_sync.sv
40
+
41
+ ## Basic CLI usage:
42
+ eda <command> [--args] [+incdir+DIR|+define+KEY=VALUE|+plusarg=value] file1.sv file2.sv file3.sv
43
+ ```
44
+
45
+ ## Example Regression testing
46
+
47
+ ```
48
+ eda multi sim '.../*_test' --parallel 16
49
+ eda multi elab 'lib/*' --parallel 16
50
+ eda multi elab 'sim/*' --parallel 16
51
+ eda multi elab 'top/*' --parallel 16
52
+ ```
53
+
54
+ If you'd like output to stdout, which is how our github Actions are run, use `--verbose` with `eda multi`
55
+ ```
56
+ eda multi sim --verbose lib/tests/oc*test
57
+ ```
58
+
59
+ ## Example "sweep"
60
+
61
+ Sweeping a build across a range of parameters
62
+
63
+ ```
64
+ eda sweep build u200 --seed=SEED "SEED=(1,2)" +define+OC_MEMORY_BIST_PORT_COUNT=PORTS "PORTS=[1,4,8,16,32]" +define+TARGET_PLL0_CLK_HZ=MHZ000000 "MHZ=(200,400,50)" --parallel 12
65
+ ```
66
+
67
+ ## Example for building treating non .sv file(s) as systemverilog
68
+
69
+ ```
70
+ eda sim --tool verilator sv@several_modules.txt --top=my_module
71
+ ```
72
+
73
+ Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` if the file contents do not match their filename extension, and you would like `eda` to force use that file as, for example, systemverilog.
74
+
75
+
76
+ # Help
77
+
78
+ ```
79
+ eda help
80
+ ```
81
+
82
+ ```
83
+ $ eda help
84
+ INFO: [EDA] eda: version X.Y.Z
85
+ INFO: [EDA] eda_config: --config-yml=eda_config_defaults.yml observed
86
+ INFO: [EDA] eda_config: using config: ..../site-packages/opencos/eda_config_defaults.yml
87
+ INFO: [EDA] *** OpenCOS EDA ***
88
+ INFO: [EDA] Detected slang (/usr/local/bin/slang), auto-setting up tool slang
89
+ INFO: [EDA] Detected verilator (/usr/local/bin/verilator), auto-setting up tool verilator
90
+ INFO: [EDA] Detected surelog (/usr/local/bin/surelog), auto-setting up tool surelog
91
+ INFO: [EDA] Detected gtkwave (/usr/bin/gtkwave), auto-setting up tool gtkwave
92
+ INFO: [EDA] Detected vivado (/tools/Xilinx/Vivado/VVVV.v/bin/vivado), auto-setting up tool vivado
93
+ INFO: [EDA] Detected slang_yosys (/usr/local/bin/yosys), auto-setting up tool slang_yosys
94
+ INFO: [EDA] Detected iverilog (/usr/local/bin/iverilog), auto-setting up tool iverilog
95
+
96
+ Usage:
97
+ eda [<options>] <command> [options] <files|targets, ...>
98
+
99
+ Where <command> is one of:
100
+
101
+ sim - Simulates a DEPS target
102
+ elab - Elaborates a DEPS target (sort of sim based LINT)
103
+ synth - Synthesizes a DEPS target
104
+ flist - Create dependency from a DEPS target
105
+ proj - Create a project from a DEPS target for GUI sim/waves/debug
106
+ multi - Run multiple DEPS targets, serially or in parallel
107
+ tools-multi - Same as 'multi' but run on all available tools, or specfied using --tools
108
+ sweep - Sweep one or more arguments across a range, serially or in parallel
109
+ build - Build for a board, creating a project and running build flow
110
+ waves - Opens waveform from prior simulation
111
+ upload - Uploads a finished design into hardware
112
+ open - Opens a project
113
+ export - Export files related to a target, tool independent
114
+ help - This help (without args), or i.e. "eda help sim" for specific help
115
+
116
+ And <files|targets, ...> is one or more source file or DEPS markup file target,
117
+ such as .v, .sv, .vhd[l], .cpp files, or a target key in a DEPS.[yml|yaml|toml|json].
118
+ Note that you can prefix source files with `sv@`, `v@`, `vhdl@` or `cpp@` to
119
+ force use that file as systemverilog, verilog, vhdl, or C++, respectively.
120
+
121
+
122
+ opencos common options:
123
+ --version
124
+ --color, --no-color Use shell colors for info/warning/error messaging (default: True)
125
+ --quiet, --no-quiet Do not display info messaging
126
+ --verbose, --no-verbose
127
+ Display additional messaging level 2 or higher
128
+ --fancy, --no-fancy
129
+ --debug, --no-debug Display additional debug messaging level 1 or higher
130
+ --debug-level DEBUG_LEVEL
131
+ Set debug level messaging (default: 0)
132
+ --logfile LOGFILE Write eda messaging to logfile (default disabled)
133
+ --force-logfile FORCE_LOGFILE
134
+ Set to force overwrite the logfile
135
+ --no-respawn Legacy mode (default respawn disabled) for respawning eda.py using $OC_ROOT/bin
136
+
137
+ opencos eda config options:
138
+ --config-yml CONFIG_YML
139
+ YAML filename to use for configuration (default eda_config_defaults.yml)
140
+
141
+ eda options:
142
+ -q, --quit For interactive mode (eda called with no options, command, or targets)
143
+ --exit same as --quit
144
+ -h, --help
145
+ --tool TOOL Tool to use for this command, such as: modelsim_ase, verilator, modelsim_ase=/path/to/bin/vsim, verilator=/path/to/bin/verilator
146
+ --eda-safe disable all DEPS file deps shell commands, overrides values from --config-yml
147
+ ```
opencos/docs/oc_cli.md ADDED
@@ -0,0 +1,135 @@
1
+
2
+ USAGE
3
+ =====
4
+
5
+ [sudo] oc_cli --serial <port>
6
+
7
+ <port> can be COM<x> (Windows) or /dev/ttyUSB<x> (Linux). Depending on permissions for /dev/ttyUSB<x>, sudo maybe required.
8
+
9
+ <port> can also be "tcp:<ip>:<port>:<device>" for connecting via TCP-to-UART bridge.
10
+
11
+ GLOBAL COMMANDS
12
+ ===============
13
+
14
+ info
15
+ ^^ prints compile-time info (UUID, build time/date, included userspace, etc)
16
+
17
+ scan
18
+ ^^ scans the top level (ring) interfaces and reports the type of IP on each channel
19
+
20
+ perf (NOT PORTED YET)
21
+ ^^ does a performance test of the comms channel
22
+
23
+ debug <n>
24
+ ^^ set debug level <n>. without argument, reports debug level.
25
+
26
+ ping
27
+ ^^ sends <CR> and checks for a prompt, sends "^" and checks for a syntax error response.
28
+
29
+ reset
30
+ ^^ sends 64x '!' characters, which initiates a full chip reset regardless of the state of the device.
31
+
32
+ source <script file>
33
+ ^^ reads in a script with oc_cli commands
34
+
35
+ import <python file>
36
+ ^^ reads in Python code to extend the commands oc_cli understands. Typically used to pull in a userspace "driver". Can also be used to "live patch" oc_cli during development work.
37
+
38
+ set <var> <value>
39
+ ^^ sets a variable for use in oc_cli commands. Refer to variables with $<var> syntax. Typically used to enable scripts (see 'source') that are independent of channel allocation in the target (for example, 'set user_ch 4' and then a script doing 'memtest $user_ch xxx')
40
+
41
+ read|rd|r <address> [<channel> <address space>]
42
+ ^^ reads a CSR in the device. <channel> and <address space> are optional after the first rd/wr command; if not provided, the prior values will be used.
43
+
44
+ write|wr|w <address> <data> [<channel> <address space>]
45
+ ^^ writes a CSR in the device. <channel> and <address space> are optional after the first rd/wr command; if not provided, the prior values will be used.
46
+
47
+ quit|q
48
+ ^^ exit oc_cli
49
+
50
+ PLL COMMANDS
51
+ ============
52
+
53
+ PLL commands are valid for channels that have a PLL IP (use 'scan' if not sure)
54
+
55
+ pll <channel>
56
+ ^^ dumps the state of the PLL
57
+
58
+ pll <channel> measure
59
+ ^^ measure clock 0 (for now) of the PLL on <channel>
60
+
61
+ pll <channel> reset
62
+ ^^ reset PLL on <channel>. Generally required after changing multipliers, dividers, etc.
63
+
64
+ pll <channel> all_up (or all_down)
65
+ ^^ move all clocks on PLL up (or down) in frequency, by changing the feedback divider. does not bounds check.
66
+
67
+ pll <channel> clk0_up (or clk0_down)
68
+ ^^ move CLK0 on PLL up (or down) in frequency, by changing the fractional divider. does not bounds check.
69
+
70
+ pll <channel> throttle <level>
71
+ ^^ throttles the output clock 0 (for now). <level> is 0-8, in 12.5% steps, with 0 meaning "no throttling" and 8 meaning "complete stop".
72
+
73
+ pll <channel> freq <mhz>
74
+ ^^ attempts to set the clock of PLL <channel> to <mhz>. NOT IMPLEMENTED YET!!! (it's non-trivial)
75
+
76
+ pll <channel> ramp <command list>
77
+ ^^ runs <command list> (an arbitrary oc_cli command line) at the current clock speeds, then ramps up the speed by reducing the fractional divider on the clock. This will test some limited range (for example 350-410MHz). To go beyond this, manually changing VCO freq, dividers, etc, will be required.
78
+
79
+
80
+ CHIPMON COMMANDS
81
+ ================
82
+
83
+ CHIPMON commands are valid for channels that have a CHIPMON IP (use 'scan' if not sure)
84
+
85
+ chipmon <channel>
86
+ ^^ dumps the state of the CHIPMON
87
+
88
+
89
+ PROTECT COMMANDS
90
+ ================
91
+
92
+ PROTECT commands are valid for channels that have a PROTECT IP (use 'scan' if not sure)
93
+
94
+ protect <channel>
95
+ ^^ dumps the state of the PROTECT, including the bitstream ID and the FPGA's unique DNA fingerprint (both of which usually required to get a license)
96
+
97
+ protect <channel> unlock <key>
98
+ ^^ unlocks the bitstream protection using <key>
99
+
100
+ MEMTEST COMMANDS
101
+ ================
102
+
103
+ MEMTEST is available in the default userspace, and will not be in other userspaces unless specifically included (in which case, there maybe other commands needed to configure the userspace to connect memtest to the memory channels being tested).
104
+
105
+ memtest <channel>
106
+ ^^ dumps the state of the MEMTEST
107
+
108
+ memtest <channel> <args>
109
+ ^^ <args> is:
110
+ write - enables writing memory
111
+ read - enables reading memory
112
+ verbose=<x> - set verbosity to <n> (default 1)
113
+ ops=<x> - run <x> operations per port under test (default 1)
114
+ addr=<x> - <x> is the base address in memory (default 0)
115
+ addr_incr=<x> - <x> as the amount to increment per op (default 0x20, 32 Bytes)
116
+ addr_incr_mask=<x> - <x> is a bitmask applied to the increment value before ORing with addr (default 0xffffffffffffffff)
117
+ addr_rand_mask=<x> - <x> is a bitmask applied to a random value before ORing with addr (default 0)
118
+ addr_port_shift=<x> - <x> is the amount to shift the port number to the left before ORing with addr (default 0)
119
+ addr_port_mask=<x> - <x> is a bitmask applied to the shifted port number before ORing with addr (default 0)
120
+ waits=<x> - <x> is the number of waitstates between ops (default 0)
121
+ burst=<x> - <x> is the number of words in each op (default 1). Note the CSR actually will hold (burst-1).
122
+ pattern=<x> - <x> is a 32-bit seed for pattern generation (used to generate write data) (default 0x11111111).
123
+ signature=<x> - <x> is the expected signature (which is a combination of all read data)
124
+ write_max_id - <x> is the highest AXI ID used for issuing writes (default 0)
125
+ read_max_id - <x> is the highest AXI ID used for issuing reads (default 0)
126
+ prescale - sets the counter prescaler, will divide counters by N so that they can be sampled at the end of long tests
127
+ write_len_rand - enables random write length
128
+ read_len_rand - enables random read length
129
+ write_data_rotate - enables write data rotation, ensures bus toggling without random data (which can be harder to make sense of in waves)
130
+ write_data_random - enables write data randomization (still 'pseudorandom' and will be repeatable in terms of read signature)
131
+ measure - report AXI-Lite and AXI3 clock speeds, cycles under reset, and cycles since reset
132
+ nopoll - kick off the MEMTEST engine, but don't wait for completion, so oc_cli can be used to monitor temps, etc
133
+ stats - report stats (latency, contexts) in summary, or per-port (verbose>1)
134
+ get_sigs - report per-port signatures
135
+ signature_<p>=<x> - set port <p> expected signature to <x> (default for all ports is -1, which means don't check)
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())
@@ -204,9 +226,12 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
204
226
 
205
227
  has_all_exe = True
206
228
  has_all_in_exe_path = True
229
+ exe_path = None
207
230
  for exe in exe_list:
208
231
  assert exe != '', f'{name=} {value=} value missing "exe" {exe=}'
209
- p = shutil.which(exe)
232
+ p = safe_shutil_which(exe)
233
+ if not exe_path:
234
+ exe_path = p # set on first required exe
210
235
  if not p:
211
236
  has_all_exe = False
212
237
  util.debug(f"... No, missing exe {exe}")
@@ -217,10 +242,11 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
217
242
 
218
243
  has_vsim_helper = True
219
244
  if value.get('requires_vsim_helper', False):
220
- # This tool name must be in opencos.utils.vsim_helper.TOOL_IS[name].
245
+ # This tool name must be in opencos.utils.vsim_helper.TOOL_PATH[name].
221
246
  # Special case for vsim being used by a lot of tools.
222
247
  vsim_helper.init() # only runs checks once internally
223
- has_vsim_helper = vsim_helper.TOOL_IS.get(name, False)
248
+ exe_path = vsim_helper.TOOL_PATH[name]
249
+ has_vsim_helper = bool(exe_path)
224
250
 
225
251
  has_vscode_helper = True
226
252
  needs_vscode_extensions = value.get('requires_vscode_extension', None)
@@ -254,18 +280,28 @@ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-man
254
280
 
255
281
  if all((has_all_py, has_all_env, has_all_exe, has_all_in_exe_path,
256
282
  has_vsim_helper, has_vscode_helper)):
257
- exe = exe_list[0]
258
- p = shutil.which(exe)
259
- config['auto_tools_found'][name] = exe # populate key-value pairs w/ first exe in list
283
+ if exe_path:
284
+ p = exe_path
285
+ else:
286
+ p = safe_shutil_which(exe_list[0])
287
+ config['auto_tools_found'][name] = p # populate key-value pairs w/ first exe in list
260
288
  if not quiet:
261
289
  util.info(f"Detected {name} ({p})")
262
- tool_setup(tool=name, quiet=True, auto_setup=True, warnings=warnings, config=config)
290
+ tool_setup(
291
+ tool=name, quiet=True, auto_setup=(not tool), warnings=warnings, config=config
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=}')
263
297
 
264
298
  return config
265
299
 
266
300
 
267
- def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool = False,
268
- warnings: bool = True):
301
+ def tool_setup( # pylint: disable=too-many-branches
302
+ tool: str, config: dict, quiet: bool = False, auto_setup: bool = False,
303
+ warnings: bool = True
304
+ ):
269
305
  ''' Adds items to config["tools_loaded"] (set) and updates config['command_handler'].
270
306
 
271
307
  config is potentially updated for entry ['command_handler'][command] with a Tool class.
@@ -322,10 +358,22 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
322
358
 
323
359
  cls = util.import_class_from_string(str_class_name)
324
360
 
325
- assert issubclass(cls, Tool), \
326
- f'{str_class_name=} is does not have Tool class associated with it'
327
- util.debug(f'Setting {cls=} for {command=} in config.command_handler')
328
- 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
329
377
 
330
378
  config['tools_loaded'].add(tool)
331
379
 
@@ -348,7 +396,6 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
348
396
  deferred_tokens = []
349
397
  command = ""
350
398
  run_auto_tool_setup = True
351
- process_tokens_cwd = os.getcwd()
352
399
 
353
400
  parser = eda_base.get_argparser()
354
401
  try:
@@ -375,6 +422,8 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
375
422
 
376
423
  # Attempt to get the 'command' in the unparsed args before we've even
377
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.
378
427
  for value in unparsed:
379
428
  if value in config['DEFAULT_HANDLERS'].keys():
380
429
  command = value
@@ -390,7 +439,7 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
390
439
  # This will handle any --tool=<name>=/path/to/bin also, so don't have to
391
440
  # run tool_setup(..) on its own.
392
441
  config = init_config(
393
- config, tool=parsed.tool,
442
+ config, tool=parsed.tool, command=command,
394
443
  run_auto_tool_setup=run_auto_tool_setup
395
444
  )
396
445
  if not config:
@@ -457,26 +506,82 @@ def process_tokens( # pylint: disable=too-many-branches,too-many-statements,too-
457
506
  util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
458
507
 
459
508
  if rc == 0 and not parsed.tool and getattr(sco, 'tool_changed_respawn', False):
460
- use_tool = sco.args.get('tool', '')
461
- if not use_tool:
462
- util.error(f'Unable to change tool from {parsed.tool}, internal eda problem.')
463
- return status_constants.EDA_DEFAULT_ERROR
464
-
465
- # close the util.log:
466
- util.stop_log()
467
- # respawn the original job, but with --tool=<use_tool> applied:
468
- _command_list = [get_eda_exec(command), f"--tool={use_tool}"] + original_args
469
- util.info(f'eda: respawn for tool change: {" ".join(_command_list)};',
470
- f' (running from: {process_tokens_cwd})')
471
- subprocess_run_background(
472
- work_dir=process_tokens_cwd,
473
- command_list=_command_list,
474
- 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
475
512
  )
476
513
 
477
514
  return rc
478
515
 
479
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
+
480
585
  def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> int:
481
586
  '''Returns bash/sh return code, checks that a command handling class has all
482
587
 
@@ -514,7 +619,7 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
514
619
  if k == 'exe' or k.startswith('requires_cmd'):
515
620
  util.warning(
516
621
  f"tool '{parsed_tool}' has requirements that may not have been met --",
517
- f"{k}: {v}"
622
+ f"{k}: {v} (Perhaps not in PATH?)"
518
623
  )
519
624
  if k == 'requires_vsim_helper':
520
625
  if found_tool := vsim_helper.found():
@@ -585,7 +690,8 @@ def main(*args):
585
690
  if not util.args['quiet']:
586
691
  util.info(f'eda: version {opencos.__version__}', color=Colors.bcyan)
587
692
  # And show the command that was run (all args):
588
- 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()})")
589
695
 
590
696
  # Handle --config-yml= arg
591
697
  config, unparsed = eda_config.get_eda_config(unparsed)
@@ -613,13 +719,41 @@ def main(*args):
613
719
  util.global_log.stop()
614
720
  return main_ret
615
721
 
722
+ def main_show_autocomplete_instructions() -> None:
723
+ ''' Executable script entry point - eda_show_autocomplete
724
+
725
+ from pyproject.toml::project.scripts. Shows instructions on how to enable bash autocomplete
726
+ '''
727
+ source_filepath = opencos.__file__.replace(
728
+ '__init__.py', 'eda_deps_bash_completion.bash'
729
+ )
730
+ if os.path.exists(source_filepath):
731
+ print(
732
+ f"{Colors.yellow}To enable bash autocompletion with"
733
+ f" {Colors.bold}eda{Colors.normal}{Colors.yellow} script (uv not equired):"
734
+ )
735
+ print(f"{Colors.normal}")
736
+ print(f" source {source_filepath}")
737
+ print("")
738
+ print(f"{Colors.yellow}Feel free to inspect this script prior to sourcing.")
739
+ print(f"{Colors.normal}")
740
+ sys.exit(0)
741
+ else:
742
+ util.error(f"It appears the following file(s) doe not exist: {source_filepath}")
743
+ sys.exit(1)
744
+
616
745
 
617
746
  def main_cli() -> None:
618
- ''' Returns None, will exit with return code. Entry point for package script or __main__.'''
747
+ ''' Executable script entry point - eda
748
+
749
+ from pyproject.tom::project.scripts, or from __main__.
750
+ Returns None, will exit with return code.
751
+ '''
619
752
  signal.signal(signal.SIGINT, signal_handler)
620
753
  util.global_exit_allowed = True
621
754
  # Strip eda or eda.py from sys.argv, we know who we are if called from __main__:
622
755
  rc = main()
756
+ subprocess_helpers.cleanup_all()
623
757
  util.exit(rc)
624
758
 
625
759