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.
- opencos/commands/deps_help.py +63 -113
- opencos/commands/export.py +7 -2
- opencos/commands/multi.py +4 -4
- opencos/commands/sim.py +14 -15
- opencos/commands/sweep.py +1 -1
- opencos/commands/synth.py +1 -2
- opencos/commands/upload.py +192 -4
- opencos/commands/waves.py +52 -8
- opencos/deps/deps_commands.py +6 -6
- opencos/deps/deps_processor.py +129 -50
- opencos/docs/Architecture.md +45 -0
- opencos/docs/ConnectingApps.md +29 -0
- opencos/docs/DEPS.md +199 -0
- opencos/docs/Debug.md +77 -0
- opencos/docs/DirectoryStructure.md +22 -0
- opencos/docs/Installation.md +117 -0
- opencos/docs/OcVivadoTcl.md +63 -0
- opencos/docs/OpenQuestions.md +7 -0
- opencos/docs/README.md +13 -0
- opencos/docs/RtlCodingStyle.md +54 -0
- opencos/docs/eda.md +147 -0
- opencos/docs/oc_cli.md +135 -0
- opencos/eda.py +358 -155
- opencos/eda_base.py +187 -60
- opencos/eda_config.py +70 -35
- opencos/eda_config_defaults.yml +310 -186
- opencos/eda_config_reduced.yml +19 -39
- opencos/eda_tool_helper.py +190 -21
- opencos/files.py +26 -1
- opencos/tools/cocotb.py +11 -23
- opencos/tools/invio.py +2 -2
- opencos/tools/invio_yosys.py +2 -1
- opencos/tools/iverilog.py +3 -3
- opencos/tools/modelsim_ase.py +1 -1
- opencos/tools/quartus.py +172 -137
- opencos/tools/questa_common.py +50 -9
- opencos/tools/riviera.py +5 -4
- opencos/tools/slang.py +14 -10
- opencos/tools/slang_yosys.py +1 -0
- opencos/tools/surelog.py +7 -6
- opencos/tools/verilator.py +9 -7
- opencos/tools/vivado.py +315 -180
- opencos/tools/yosys.py +5 -5
- opencos/util.py +6 -3
- opencos/utils/dict_helpers.py +31 -0
- opencos/utils/markup_helpers.py +2 -2
- opencos/utils/str_helpers.py +38 -0
- opencos/utils/subprocess_helpers.py +3 -3
- opencos/utils/vscode_helper.py +2 -2
- opencos/utils/vsim_helper.py +16 -5
- {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/METADATA +1 -1
- opencos_eda-0.3.12.dist-info/RECORD +93 -0
- opencos/eda_config_max_verilator_waivers.yml +0 -39
- opencos_eda-0.3.10.dist-info/RECORD +0 -81
- {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/WHEEL +0 -0
- {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/entry_points.txt +0 -0
- {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.3.10.dist-info → opencos_eda-0.3.12.dist-info}/top_level.txt +0 -0
opencos/commands/deps_help.py
CHANGED
|
@@ -13,11 +13,70 @@ uses no tools and will print a help text regarding DEPS markup files to stdout.
|
|
|
13
13
|
# pylint: disable=line-too-long
|
|
14
14
|
|
|
15
15
|
import os
|
|
16
|
+
import re
|
|
16
17
|
|
|
17
18
|
from opencos.eda_base import Command
|
|
18
19
|
from opencos import util
|
|
19
20
|
from opencos.util import Colors
|
|
20
21
|
|
|
22
|
+
def get_deps_md_file() -> str:
|
|
23
|
+
'''Tries to get docs/DEPS.md from our pypackage dist'''
|
|
24
|
+
opencos_dir, _ = os.path.split(util.__file__)
|
|
25
|
+
|
|
26
|
+
# Try to get it from site-packages dir, which should have docs/ alongside
|
|
27
|
+
# commands/ and tools/
|
|
28
|
+
filename = os.path.join(opencos_dir, 'docs', 'DEPS.md')
|
|
29
|
+
if os.path.isfile(filename):
|
|
30
|
+
return filename
|
|
31
|
+
|
|
32
|
+
# If you're running directly from the git checkout dir, you won't be getting
|
|
33
|
+
# this dist, so it's not in opencos/docs, it will simply be in ./docs
|
|
34
|
+
filename = os.path.join(opencos_dir, '..', 'docs', 'DEPS.md')
|
|
35
|
+
if os.path.isfile(filename):
|
|
36
|
+
return filename
|
|
37
|
+
return ''
|
|
38
|
+
|
|
39
|
+
def get_deps_md_contents() -> str:
|
|
40
|
+
'''Tries to get the docs/DEPS.md file and returns the str contents
|
|
41
|
+
|
|
42
|
+
This also performs some limited colorization of markdown and YAML
|
|
43
|
+
(assuming util was not disabled with --no-color)
|
|
44
|
+
'''
|
|
45
|
+
filename = get_deps_md_file()
|
|
46
|
+
if not filename:
|
|
47
|
+
return ''
|
|
48
|
+
|
|
49
|
+
def make_byellow(match):
|
|
50
|
+
'''Used by re.sub to wrap the match with bold yellow and return to normal yellow'''
|
|
51
|
+
return f'{Colors.byellow}{match.group(0)}{Colors.normal}{Colors.yellow}'
|
|
52
|
+
|
|
53
|
+
lines = []
|
|
54
|
+
with open(filename, encoding='utf-8') as f:
|
|
55
|
+
for line in f.readlines():
|
|
56
|
+
|
|
57
|
+
if line.startswith('# '):
|
|
58
|
+
# colors for markdown headings
|
|
59
|
+
line = f'{Colors.bgreen}{line}{Colors.normal}{Colors.yellow}'
|
|
60
|
+
elif line.startswith('## '):
|
|
61
|
+
# colors for markdown headings
|
|
62
|
+
line = f'{Colors.bcyan}{line}{Colors.normal}{Colors.yellow}'
|
|
63
|
+
|
|
64
|
+
elif '#' in line:
|
|
65
|
+
# colors for comments
|
|
66
|
+
line = line.replace('#', f'{Colors.normal}{Colors.cyan}#') + Colors.yellow
|
|
67
|
+
|
|
68
|
+
# colors for starting a line with:
|
|
69
|
+
# key: value
|
|
70
|
+
# - key : value
|
|
71
|
+
# try to make "key:" or "- key:" as bold:
|
|
72
|
+
line = re.sub(
|
|
73
|
+
r'^( *\-? ?[^ ]+):', make_byellow, line
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
lines.append(line)
|
|
77
|
+
|
|
78
|
+
return ''.join(lines)
|
|
79
|
+
|
|
21
80
|
|
|
22
81
|
BASIC_DEPS_HELP = f'''
|
|
23
82
|
{Colors.yellow}
|
|
@@ -128,119 +187,8 @@ FULL_DEPS_HELP = f'''
|
|
|
128
187
|
|
|
129
188
|
{Colors.green}--------------------------------------------------------------------{Colors.yellow}
|
|
130
189
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
DEFAULTS: # <table> defaults applied to ALL targets in this file, local targets ** override ** the defaults.
|
|
135
|
-
|
|
136
|
-
METADATA: # <table> unstructured data, any UPPERCASE first level key is not considered a target.
|
|
137
|
-
|
|
138
|
-
target-spec:
|
|
139
|
-
|
|
140
|
-
args: # <array or | separated str>
|
|
141
|
-
- --waves
|
|
142
|
-
- --sim_plusargs="+info=500"
|
|
143
|
-
|
|
144
|
-
defines: # <table>
|
|
145
|
-
SOME_DEFINE: value
|
|
146
|
-
SOME_DEFINE_NO_VALUE: # we just leave this blank, or use nil (yaml's None)
|
|
147
|
-
|
|
148
|
-
plusargs: # <table>
|
|
149
|
-
variable0: value
|
|
150
|
-
variable1: # blank for no value, or use nil (yaml's None)
|
|
151
|
-
|
|
152
|
-
parameters: # <table>
|
|
153
|
-
SomeParameter: value
|
|
154
|
-
SOME_OTHER_PARAMETER: value
|
|
155
|
-
|
|
156
|
-
incdirs: # <array>
|
|
157
|
-
- some/relative/path
|
|
158
|
-
|
|
159
|
-
top: # <string>
|
|
160
|
-
|
|
161
|
-
deps: # <array or | space separated string>
|
|
162
|
-
- some_relative_target # <string> aka, a target
|
|
163
|
-
- some_file.sv # <string> aka, a file
|
|
164
|
-
- sv@some_file.txt # <string> aka, ext@file where we'd like a file not ending in .sv to be
|
|
165
|
-
# treated as a .sv file for tools.
|
|
166
|
-
# Supported for sv@, v@, vhdl@, cpp@, sdc@, f@, py@, makefile@
|
|
167
|
-
- commands: # <table> with key 'commands' for a <array>: support for built-in commands
|
|
168
|
-
# Note this cannot be confused for other targets or files.
|
|
169
|
-
- shell: # <string>
|
|
170
|
-
var-subst-args: # <bool> default false. If true, substitute vars in commands, such as {{fpga}}
|
|
171
|
-
# substituted from eda arg --fpga=SomeFpga, such that {{fpga}} becomes SomeFpga
|
|
172
|
-
var-subst-os-env: #<bool> default false. If true, substitute vars in commands using os.environ vars,
|
|
173
|
-
# such as {{FPGA}} could get substituted by env value for $FPGA
|
|
174
|
-
tee: # <string> optional filename, otherwise shell commands write to {{target-spec}}__shell_0.log
|
|
175
|
-
run-from-work-dir: #<bool> default true. If false, runs from the directory of this DEPS file.
|
|
176
|
-
filepath-subst-target-dir: #<bool> default true. If false, disables shell file path
|
|
177
|
-
substituion on this target's directory (this DEPS file dir).
|
|
178
|
-
dirpath-subst-target-dir: #<bool> default false. If true, enables shell directory path
|
|
179
|
-
substituion on this target's directory (this DEPS file dir).
|
|
180
|
-
run-after-tool: # <bool> default false. Set to true to run after any EDA tools, or
|
|
181
|
-
any command handlers have completed.
|
|
182
|
-
- shell: echo "Hello World!"
|
|
183
|
-
- work-dir-add-sources: # <array or | space separated string>, this is how to add generated files
|
|
184
|
-
# to compile order list.
|
|
185
|
-
- peakrdl: # <string> ## peakrdl command to generate CSRs
|
|
186
|
-
|
|
187
|
-
reqs: # <array or | space separated string>
|
|
188
|
-
- some_file.mem # <string> aka, a non-source file required for this target.
|
|
189
|
-
# This file is checked for existence prior to invoking the tool involved, for example,
|
|
190
|
-
# in a simulation this would be done prior to a compile step.
|
|
191
|
-
|
|
192
|
-
multi:
|
|
193
|
-
ignore-this-target: # <array of tables> eda commands to be ignored in `eda multi <command>` for this target only
|
|
194
|
-
# this is checked in the matching multi targets list, and is not inherited through dependencies.
|
|
195
|
-
- commands: synth # space separated strings
|
|
196
|
-
tools: vivado # space separated strings
|
|
197
|
-
|
|
198
|
-
- commands: sim # omit tools, ignores 'sim' commands for all tools, for this target only, when this target
|
|
199
|
-
# is in the target list called by `eda multi`.
|
|
200
|
-
|
|
201
|
-
- tools: vivado # omit commands, ignores all commands if tool is vivado, for this target only, when this target
|
|
202
|
-
# is in the target list called by `eda multi`.
|
|
203
|
-
|
|
204
|
-
args: # <array> additional args added to all multi commands of this target.
|
|
205
|
-
# Note that all args are POSIX with dashes, --sim-plusargs=value, etc.
|
|
206
|
-
|
|
207
|
-
<eda-command>: # key is one of sim, flist, build, synth, etc.
|
|
208
|
-
# can be used instead of 'tags' to support different args or deps.
|
|
209
|
-
disable-tools: # Note: not implemented yet.
|
|
210
|
-
only-tools: # Note: not implemented yet.
|
|
211
|
-
args: # <array or | space separated string>
|
|
212
|
-
deps: # <array or | space separated string> # Note: not implemented yet
|
|
213
|
-
defines: ## <table>
|
|
214
|
-
plusargs: ## <table>
|
|
215
|
-
parameters: ## <table>
|
|
216
|
-
incdirs: ## <array>
|
|
217
|
-
|
|
218
|
-
tags: # <table> this is the currently support tags features in a target.
|
|
219
|
-
<tag-name>: # <string> key for table, can be anything, name is not used.
|
|
220
|
-
with-tools: <array or | space separated string>
|
|
221
|
-
# If using one of these tools, apply these values.
|
|
222
|
-
# entries can be in the form: vivado, or vivado:2024.1
|
|
223
|
-
with-commands: <array or | space separated string>
|
|
224
|
-
# apply if this was the `eda` command, such as: sim
|
|
225
|
-
with-args: # <table> (optional) arg key/value pairs to match for this tag.
|
|
226
|
-
# this would be an alternative to running eda with --tags=value
|
|
227
|
-
# The existence of an argument with correct value would enable a tag.
|
|
228
|
-
# And example would be:
|
|
229
|
-
# with-args:
|
|
230
|
-
# waves: true
|
|
231
|
-
args: <array or | space separated string> # args to be applied if this target is used, with a matching
|
|
232
|
-
# tool in 'with-tools'.
|
|
233
|
-
deps: <array or | space separated string, applied with tag>
|
|
234
|
-
defines: <table, applied with tag>
|
|
235
|
-
plusargs: <table, applied with tag>
|
|
236
|
-
parameters: <table, applied with tag>
|
|
237
|
-
incdirs: <array, applied with tag>
|
|
238
|
-
replace-config-tools: <table> # spec matching eda_config_defaults.yml::tools.<tool> (replace merge strategy)
|
|
239
|
-
additive-config-tools: <table> # spec matching eda_config_defaults.yml::tools.<tool> (additive merge strategy)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
```
|
|
243
|
-
'''
|
|
190
|
+
''' + get_deps_md_contents()
|
|
191
|
+
|
|
244
192
|
|
|
245
193
|
|
|
246
194
|
class CommandDepsHelp:
|
|
@@ -275,4 +223,6 @@ class CommandDepsHelp:
|
|
|
275
223
|
'''
|
|
276
224
|
Command.help(self, tokens=tokens, no_targets=True)
|
|
277
225
|
print()
|
|
226
|
+
print(BASIC_DEPS_HELP)
|
|
227
|
+
print()
|
|
278
228
|
print(FULL_DEPS_HELP)
|
opencos/commands/export.py
CHANGED
|
@@ -58,9 +58,14 @@ class CommandExport(CommandDesign):
|
|
|
58
58
|
|
|
59
59
|
def do_it(self) -> None:
|
|
60
60
|
|
|
61
|
-
# decide output dir name
|
|
61
|
+
# decide output dir name, note this does not follow the work-dir naming of
|
|
62
|
+
# eda.work/{target}.{command}
|
|
62
63
|
if not self.args['output']:
|
|
63
|
-
|
|
64
|
+
if self.target:
|
|
65
|
+
name = f'{self.target}.export'
|
|
66
|
+
else:
|
|
67
|
+
name = self.args.get('top', '') + '.export'
|
|
68
|
+
self.args['output'] = os.path.join('.', 'eda.export', name)
|
|
64
69
|
out_dir = self.args['output']
|
|
65
70
|
|
|
66
71
|
if not self.target:
|
opencos/commands/multi.py
CHANGED
|
@@ -6,14 +6,14 @@ These are not intended to be overriden by child classes. They do not inherit Too
|
|
|
6
6
|
import argparse
|
|
7
7
|
import glob
|
|
8
8
|
import os
|
|
9
|
-
import shutil
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
|
|
12
11
|
from opencos import util, eda_base, eda_config, export_helper, \
|
|
13
12
|
eda_tool_helper
|
|
14
|
-
from opencos.eda_base import CommandParallel, get_eda_exec
|
|
15
13
|
from opencos.deps.deps_file import get_deps_markup_file, deps_markup_safe_load, \
|
|
16
14
|
deps_data_get_all_targets, deps_list_target_sanitize
|
|
15
|
+
from opencos.eda_base import CommandParallel, get_eda_exec
|
|
16
|
+
from opencos.files import safe_shutil_which
|
|
17
17
|
from opencos.utils.str_helpers import fnmatch_or_re, dep_str2list
|
|
18
18
|
|
|
19
19
|
class CommandMulti(CommandParallel):
|
|
@@ -281,7 +281,7 @@ class CommandMulti(CommandParallel):
|
|
|
281
281
|
if parsed.parallel < 1 or parsed.parallel > 256:
|
|
282
282
|
self.error("Arg 'parallel' must be between 1 and 256")
|
|
283
283
|
|
|
284
|
-
command = self.
|
|
284
|
+
command = self.get_sub_command_from_config()
|
|
285
285
|
|
|
286
286
|
# Need to know the tool for this command, either it was set correctly via --tool and/or
|
|
287
287
|
# the command (class) will tell us.
|
|
@@ -425,7 +425,7 @@ class CommandMulti(CommandParallel):
|
|
|
425
425
|
'''
|
|
426
426
|
eda_path = get_eda_exec('multi')
|
|
427
427
|
command = self.single_command
|
|
428
|
-
timeout =
|
|
428
|
+
timeout = safe_shutil_which('timeout')
|
|
429
429
|
|
|
430
430
|
# Built-in support for running > 1 tool.
|
|
431
431
|
all_multi_tools = self.multi_which_tools(command)
|
opencos/commands/sim.py
CHANGED
|
@@ -370,7 +370,8 @@ class CommandSim(CommandDesign): # pylint: disable=too-many-public-methods
|
|
|
370
370
|
def check_logs_for_errors( # pylint: disable=dangerous-default-value,too-many-locals,too-many-branches
|
|
371
371
|
self,
|
|
372
372
|
sim_retcode: int = 0,
|
|
373
|
-
filename: str = '',
|
|
373
|
+
filename: str = '',
|
|
374
|
+
file_contents_str: str = '',
|
|
374
375
|
bad_strings: list = [], must_strings: list = [],
|
|
375
376
|
warning_strings: list = [],
|
|
376
377
|
use_bad_strings: bool = True, use_must_strings: bool = True
|
|
@@ -404,32 +405,30 @@ class CommandSim(CommandDesign): # pylint: disable=too-many-public-methods
|
|
|
404
405
|
lines = []
|
|
405
406
|
|
|
406
407
|
log_fpath = ''
|
|
407
|
-
if os.path.
|
|
408
|
+
if os.path.isfile(filename):
|
|
408
409
|
log_fpath = filename
|
|
409
410
|
|
|
410
411
|
if file_contents_str:
|
|
411
412
|
lines = file_contents_str.split('\n')
|
|
412
413
|
log_fname = log_fpath + '(STDOUT)'
|
|
413
414
|
util.debug(f'Checking log for errors: {log_fpath=} but checking from STDOUT string...')
|
|
414
|
-
elif
|
|
415
|
+
elif log_fpath:
|
|
415
416
|
log_fname = log_fpath
|
|
416
|
-
util.
|
|
417
|
-
if not os.path.exists(log_fname):
|
|
418
|
-
self.error(f'sim.check_logs_for_errors: log {log_fpath} does not exist, cannot',
|
|
419
|
-
'check it for errors or passing strings')
|
|
420
|
-
return
|
|
417
|
+
util.info(f'Checking log filename: {log_fpath}')
|
|
421
418
|
with open(log_fpath, 'r', encoding='utf-8') as f:
|
|
422
419
|
lines = f.read().splitlines()
|
|
423
420
|
else:
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
log_fname = filename
|
|
422
|
+
self.error(f'sim.check_logs_for_errors: {log_fname=} is not a file or does not exist,',
|
|
423
|
+
'and no file_contents_str exists to check')
|
|
426
424
|
|
|
427
425
|
self.update_tool_warn_err_counts_from_log_lines(
|
|
428
426
|
log_lines=lines, bad_strings=_bad_strings, warning_strings=_warning_strings
|
|
429
427
|
)
|
|
430
428
|
|
|
431
|
-
|
|
432
|
-
|
|
429
|
+
func = getattr(self, 'report_tool_warn_error_counts', None)
|
|
430
|
+
if func and isinstance(self, Tool) and callable(func):
|
|
431
|
+
self.report_tool_warn_error_counts() # pylint: disable=no-member
|
|
433
432
|
|
|
434
433
|
if sim_retcode > 0:
|
|
435
434
|
# We have to update artifacts first, have the caller set the error.
|
|
@@ -446,14 +445,14 @@ class CommandSim(CommandDesign): # pylint: disable=too-many-public-methods
|
|
|
446
445
|
hit_must_string_dict[k] = True
|
|
447
446
|
if any(bad_str in line for bad_str in _bad_strings):
|
|
448
447
|
self.error(
|
|
449
|
-
f"log {log_fname}:{lineno} contains one of {_bad_strings
|
|
448
|
+
f"log {log_fname}:{lineno} contains one of: {_bad_strings}",
|
|
450
449
|
error_code=status_constants.EDA_SIM_LOG_HAS_BAD_STRING
|
|
451
450
|
)
|
|
452
451
|
|
|
453
452
|
if any(x is None for x in hit_must_string_dict.values()):
|
|
454
453
|
self.error(
|
|
455
|
-
f"Didn't get all passing patterns in log {log_fname}: {_must_strings
|
|
456
|
-
f" {hit_must_string_dict
|
|
454
|
+
f"Didn't get all passing patterns in log {log_fname}: {_must_strings},",
|
|
455
|
+
f" {hit_must_string_dict}",
|
|
457
456
|
error_code=status_constants.EDA_SIM_LOG_MISSING_MUST_STRING
|
|
458
457
|
)
|
|
459
458
|
|
opencos/commands/sweep.py
CHANGED
|
@@ -72,7 +72,7 @@ class CommandSweep(CommandDesign, CommandParallel):
|
|
|
72
72
|
|
|
73
73
|
self.check_args()
|
|
74
74
|
|
|
75
|
-
self.single_command = self.
|
|
75
|
+
self.single_command = self.get_sub_command_from_config()
|
|
76
76
|
|
|
77
77
|
self._append_sweep_args(arg_tokens=arg_tokens)
|
|
78
78
|
|
opencos/commands/synth.py
CHANGED
|
@@ -67,9 +67,8 @@ class CommandSynth(CommandDesign):
|
|
|
67
67
|
if self.stop_process_tokens_before_do_it():
|
|
68
68
|
return unparsed
|
|
69
69
|
|
|
70
|
-
# add defines for this job type
|
|
71
70
|
if self.args['top']:
|
|
72
|
-
# create our work dir
|
|
71
|
+
# create our work dir (from self.args['top'])
|
|
73
72
|
self.create_work_dir()
|
|
74
73
|
self.run_dep_commands()
|
|
75
74
|
self.do_it()
|
opencos/commands/upload.py
CHANGED
|
@@ -4,20 +4,73 @@ Intended to be overriden by Tool based classes (such as CommandUploadVivado, etc
|
|
|
4
4
|
'''
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
7
10
|
|
|
8
11
|
from opencos.eda_base import Command, Tool
|
|
12
|
+
from opencos.util import Colors, debug, info, warning, safe_emoji, import_class_from_string
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
class CommandUpload(Command):
|
|
11
|
-
'''Base class command handler for: eda upload ...
|
|
16
|
+
'''Base class command handler for: eda upload ...
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
If a --tool arg is not specified, this is the default handler for 'eda upload'
|
|
19
|
+
and will attempt to choose a derived class based on the bit files found
|
|
20
|
+
'''
|
|
14
21
|
|
|
15
22
|
command_name = 'upload'
|
|
16
23
|
|
|
24
|
+
# SUPPORTED_TOOLS is used
|
|
25
|
+
SUPPORTED_TOOLS = {
|
|
26
|
+
'vivado': ['.bit'],
|
|
27
|
+
'quartus': ['.sof'],
|
|
28
|
+
}
|
|
29
|
+
BIT_EXT_TO_TOOL = {}
|
|
30
|
+
|
|
31
|
+
# Child classes can set SUPPORTED_BIT_EXT = ['.bit', ..] because they
|
|
32
|
+
# should only represent one tool
|
|
33
|
+
SUPPORTED_BIT_EXT = [item for value in SUPPORTED_TOOLS.values() for item in value]
|
|
34
|
+
|
|
35
|
+
|
|
17
36
|
def __init__(self, config: dict):
|
|
18
37
|
Command.__init__(self, config=config)
|
|
19
38
|
self.unparsed_args = []
|
|
20
39
|
|
|
40
|
+
self.args.update({
|
|
41
|
+
'bitfile': "",
|
|
42
|
+
'list-bitfiles': False,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
self.str_ext = '/'.join(self.SUPPORTED_BIT_EXT).replace('.', '').upper()
|
|
46
|
+
|
|
47
|
+
help_upload_tools = '|'.join(self.config.get('auto_tools_found', []))
|
|
48
|
+
if not help_upload_tools:
|
|
49
|
+
help_upload_tools = 'TOOL'
|
|
50
|
+
|
|
51
|
+
self.args_help.update({
|
|
52
|
+
'bitfile': (
|
|
53
|
+
f'Tool specific {self.str_ext} files to upload (auto-detected if not specified)'
|
|
54
|
+
' If you would like see full help for a given tool, use:'
|
|
55
|
+
f' {Colors.yellow}eda upload --help'
|
|
56
|
+
f' {Colors.byellow}--tool={help_upload_tools}{Colors.green}'
|
|
57
|
+
),
|
|
58
|
+
'list-bitfiles': (
|
|
59
|
+
f'List available {self.str_ext} files.'
|
|
60
|
+
' If you would like see full help for a given tool, use:'
|
|
61
|
+
f' {Colors.yellow}eda upload --help'
|
|
62
|
+
f' {Colors.byellow}--tool={help_upload_tools}{Colors.green}'
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
self.bitfiles = []
|
|
67
|
+
|
|
68
|
+
if not getattr(self, '_TOOL', ''):
|
|
69
|
+
for tool, bit_exts in self.SUPPORTED_TOOLS.items():
|
|
70
|
+
for ext in bit_exts:
|
|
71
|
+
self.BIT_EXT_TO_TOOL[ext] = tool
|
|
72
|
+
|
|
73
|
+
|
|
21
74
|
def process_tokens(
|
|
22
75
|
self, tokens: list, process_all: bool = True, pwd: str = os.getcwd()
|
|
23
76
|
) -> list:
|
|
@@ -25,9 +78,144 @@ class CommandUpload(Command):
|
|
|
25
78
|
self.unparsed_args = Command.process_tokens(
|
|
26
79
|
self, tokens=tokens, process_all=False, pwd=pwd
|
|
27
80
|
)
|
|
81
|
+
|
|
28
82
|
if self.stop_process_tokens_before_do_it():
|
|
29
83
|
return []
|
|
30
84
|
|
|
31
|
-
self.
|
|
32
|
-
|
|
85
|
+
self.bitfiles = self.get_list_bitfiles(display=True)
|
|
86
|
+
|
|
87
|
+
# If someone called --list-bitfiles, stop now.
|
|
88
|
+
if self.args['list-bitfiles']:
|
|
89
|
+
if not self.bitfiles:
|
|
90
|
+
self.error('No bitfiles found')
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
sco = self._get_child_handling_class()
|
|
94
|
+
|
|
95
|
+
if sco is None or not isinstance(sco, Tool):
|
|
96
|
+
self.error('Could not find a suitable tool to process bitfiles')
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
sco.unparsed_args = Command.process_tokens(
|
|
100
|
+
sco, tokens=tokens, process_all=False, pwd=pwd
|
|
101
|
+
)
|
|
102
|
+
sco.bitfiles = self.bitfiles
|
|
103
|
+
sco.create_work_dir()
|
|
104
|
+
sco.do_it()
|
|
33
105
|
return []
|
|
106
|
+
|
|
107
|
+
def get_targets_or_files_from_unparsed_args(self) -> (list, list):
|
|
108
|
+
'''Returns (list of targets, list of files) from unparsed args or --bitfile'''
|
|
109
|
+
|
|
110
|
+
targets = []
|
|
111
|
+
files = []
|
|
112
|
+
for f in self.unparsed_args + [self.args['bitfile']]:
|
|
113
|
+
if not f:
|
|
114
|
+
continue
|
|
115
|
+
if os.path.isfile(f):
|
|
116
|
+
files.append(f)
|
|
117
|
+
elif not f.startswith('-'):
|
|
118
|
+
# avoid a arg
|
|
119
|
+
targets.append(f)
|
|
120
|
+
return targets, files
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_list_bitfiles(self, display: bool = True) -> list:
|
|
124
|
+
'''Returns a list of bit files (ending with self.SUPPORTED_BIT_EXT)'''
|
|
125
|
+
|
|
126
|
+
bitfiles: list[Path] = []
|
|
127
|
+
|
|
128
|
+
targets, files = self.get_targets_or_files_from_unparsed_args()
|
|
129
|
+
targets.extend(files)
|
|
130
|
+
|
|
131
|
+
debug(f"Looking for bitfiles in {os.path.abspath('.')=}")
|
|
132
|
+
for root, _, files in os.walk("."):
|
|
133
|
+
for f in files:
|
|
134
|
+
if any(f.endswith(x) for x in self.SUPPORTED_BIT_EXT):
|
|
135
|
+
fullpath = os.path.abspath(Path(root) / f)
|
|
136
|
+
if os.path.isfile(fullpath) and fullpath not in bitfiles:
|
|
137
|
+
bitfiles.append(fullpath)
|
|
138
|
+
|
|
139
|
+
matched: list[Path] = []
|
|
140
|
+
for cand in bitfiles:
|
|
141
|
+
debug(f"Looking for {cand=} in {targets=}")
|
|
142
|
+
passing = all(re.search(t, str(cand)) for t in targets)
|
|
143
|
+
if passing:
|
|
144
|
+
matched.append(cand)
|
|
145
|
+
mod_time_string = datetime.fromtimestamp(
|
|
146
|
+
os.path.getmtime(cand)).strftime('%Y-%m-%d %H:%M:%S')
|
|
147
|
+
tool_guess = getattr(self, '_TOOL', '')
|
|
148
|
+
if not tool_guess:
|
|
149
|
+
ext = os.path.splitext(cand)[1]
|
|
150
|
+
tool_guess = self.BIT_EXT_TO_TOOL.get(ext, '')
|
|
151
|
+
if tool_guess:
|
|
152
|
+
tool_guess = f'({tool_guess})'
|
|
153
|
+
if display:
|
|
154
|
+
info(
|
|
155
|
+
f"{safe_emoji('⏩ ')}Found matching bitfile {tool_guess}:",
|
|
156
|
+
f"{Colors.cyan}{mod_time_string}{Colors.normal} :",
|
|
157
|
+
f"{Colors.byellow}{cand}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if display and not matched:
|
|
161
|
+
if self.args['list-bitfiles']:
|
|
162
|
+
warning(f'{safe_emoji("❕ ")}--list-bitfiles: no {self.str_ext} found that matched',
|
|
163
|
+
f'{targets}')
|
|
164
|
+
else:
|
|
165
|
+
warning(f'{safe_emoji("❕ ")} Searched for bitfiles with {self.str_ext}: none found',
|
|
166
|
+
f'that matched {targets}')
|
|
167
|
+
|
|
168
|
+
return matched
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _get_child_handling_class(self) -> object:
|
|
174
|
+
'''Returns a class handle of a child to process this, which should be a Tool class
|
|
175
|
+
|
|
176
|
+
if no appropriate child is found, returns self.
|
|
177
|
+
'''
|
|
178
|
+
|
|
179
|
+
if isinstance(self, Tool):
|
|
180
|
+
# We're already a tool handling class.
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
tools_found = set()
|
|
184
|
+
for bitfile in self.bitfiles:
|
|
185
|
+
ext = os.path.splitext(bitfile)[1]
|
|
186
|
+
tool_guess = self.BIT_EXT_TO_TOOL.get(ext, '')
|
|
187
|
+
if tool_guess:
|
|
188
|
+
tools_found.add(tool_guess)
|
|
189
|
+
else:
|
|
190
|
+
warning(f'For bitfile {bitfile} no tool found for it')
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
if not tools_found:
|
|
194
|
+
# Probably not an error?
|
|
195
|
+
warning(f'No tools found to process bitfiles: {self.bitfiles}')
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
if len(tools_found) > 1:
|
|
199
|
+
warning(f'More than one tool found ({tools_found}) to to process bitfiles:',
|
|
200
|
+
f'{self.bitfiles}')
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
tool = tools_found.pop() # only item in set
|
|
205
|
+
# Do we have a handler for this in our config?
|
|
206
|
+
if tool in self.config.get('tools_loaded', []):
|
|
207
|
+
tool_cfg = self.config.get('tools', {}).get(tool, {})
|
|
208
|
+
if tool_cfg:
|
|
209
|
+
cls_str = tool_cfg.get('handlers', {}).get(self.command_name, None)
|
|
210
|
+
if cls_str:
|
|
211
|
+
cls = import_class_from_string(cls_str)
|
|
212
|
+
if issubclass(cls, Command):
|
|
213
|
+
info(f'For found bitfiles, can use tool={tool} and handler {cls}')
|
|
214
|
+
sco = cls(config=self.config)
|
|
215
|
+
return sco
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
warning(f'No handler found for tool={tool} to process bitfiles: {self.bitfiles}')
|
|
219
|
+
debug(f'config -- tools_loaded: {self.config["tools_loaded"]}')
|
|
220
|
+
debug(f'config -- tools for tool: {self.config["tools"].get(tool, "")}')
|
|
221
|
+
return self
|
opencos/commands/waves.py
CHANGED
|
@@ -15,10 +15,12 @@ handler).
|
|
|
15
15
|
# pylint: disable=R0801
|
|
16
16
|
|
|
17
17
|
import os
|
|
18
|
-
import
|
|
18
|
+
import subprocess
|
|
19
19
|
|
|
20
20
|
from opencos import util
|
|
21
21
|
from opencos.eda_base import CommandDesign
|
|
22
|
+
from opencos.files import safe_shutil_which
|
|
23
|
+
from opencos.utils import vscode_helper
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class CommandWaves(CommandDesign):
|
|
@@ -51,6 +53,48 @@ class CommandWaves(CommandDesign):
|
|
|
51
53
|
+ ' to stdout',
|
|
52
54
|
})
|
|
53
55
|
|
|
56
|
+
def get_versions_of_tool(self, tool: str) -> str:
|
|
57
|
+
'''Similar to Tool.get_versions(), returns the version of 'tool' for tools like:
|
|
58
|
+
|
|
59
|
+
- vaporview
|
|
60
|
+
- gtkwave
|
|
61
|
+
|
|
62
|
+
This is called by eda_tool_helper.get_handler_tool_version(tool, cmd, config)
|
|
63
|
+
'''
|
|
64
|
+
|
|
65
|
+
entry = self.config.get('tools', {}).get(tool, {})
|
|
66
|
+
|
|
67
|
+
if entry and 'requires_vscode_extension' in entry:
|
|
68
|
+
# vaporview, surfer
|
|
69
|
+
vscode_ext_name = entry.get('requires_vscode_extension', [''])[0]
|
|
70
|
+
vscode_helper.init()
|
|
71
|
+
ver = vscode_helper.EXTENSIONS.get(vscode_ext_name)
|
|
72
|
+
return ver
|
|
73
|
+
|
|
74
|
+
if entry and tool == 'gtkwave':
|
|
75
|
+
# gtkwave --version is fast.
|
|
76
|
+
proc = None
|
|
77
|
+
try:
|
|
78
|
+
proc = subprocess.run(
|
|
79
|
+
[safe_shutil_which('gtkwave'), '--version'],
|
|
80
|
+
capture_output=True, check=False
|
|
81
|
+
)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
util.debug(f'gtkwave --version: exception {e}')
|
|
84
|
+
|
|
85
|
+
if not proc or not proc.stdout:
|
|
86
|
+
return ''
|
|
87
|
+
|
|
88
|
+
for line in proc.stdout.decode('utf-8', errors='replace').split('\n'):
|
|
89
|
+
if line.lower().startswith('gtkwave analyzer v'):
|
|
90
|
+
parts = line.split(' ')
|
|
91
|
+
return parts[2][1:] # trim the leading 'v' in 'v1.2.3'
|
|
92
|
+
return ''
|
|
93
|
+
|
|
94
|
+
return ''
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
54
98
|
|
|
55
99
|
def get_wave_files_in_dirs(self, wave_dirs: list, quiet: bool = False) -> list:
|
|
56
100
|
'''Returns list of all wave files give wave_dirs (list)'''
|
|
@@ -120,10 +164,10 @@ class CommandWaves(CommandDesign):
|
|
|
120
164
|
|
|
121
165
|
# TODO(drew): this feels a little customized per-tool, perhaps there's a better
|
|
122
166
|
# way to abstract this configuration for adding other waveform viewers.
|
|
123
|
-
# For example for each command we also have to check
|
|
167
|
+
# For example for each command we also have to check safe_shutil_which, because normal Tool
|
|
124
168
|
# classs should work even w/out PATH, but these don't use Tool classes.
|
|
125
169
|
if wave_file.endswith('.wdb'):
|
|
126
|
-
if 'vivado' in self.config['tools_loaded'] and
|
|
170
|
+
if 'vivado' in self.config['tools_loaded'] and safe_shutil_which('vivado'):
|
|
127
171
|
tcl_name = wave_file + '.waves.tcl'
|
|
128
172
|
with open( tcl_name, 'w', encoding='utf-8') as fo :
|
|
129
173
|
print( 'current_fileset', file=fo)
|
|
@@ -141,20 +185,20 @@ class CommandWaves(CommandDesign):
|
|
|
141
185
|
f"{self.VSIM_TOOLS} in PATH")
|
|
142
186
|
elif wave_file.endswith('.fst'):
|
|
143
187
|
if ('vaporview' in self.config['tools_loaded'] or \
|
|
144
|
-
'surfer' in self.config['tools_loaded']) and
|
|
188
|
+
'surfer' in self.config['tools_loaded']) and safe_shutil_which('code'):
|
|
145
189
|
command_list = ['code', '-n', '.', wave_file]
|
|
146
190
|
self.exec(os.path.dirname(wave_file), command_list)
|
|
147
|
-
elif 'gtkwave' in self.config['tools_loaded'] and
|
|
191
|
+
elif 'gtkwave' in self.config['tools_loaded'] and safe_shutil_which('gtkwave'):
|
|
148
192
|
command_list = ['gtkwave', wave_file]
|
|
149
193
|
self.exec(os.path.dirname(wave_file), command_list)
|
|
150
194
|
else:
|
|
151
195
|
self.error(f"Don't know how to open {wave_file} without GtkWave in PATH")
|
|
152
196
|
elif wave_file.endswith('.vcd'):
|
|
153
197
|
if ('vaporview' in self.config['tools_loaded'] or \
|
|
154
|
-
'surfer' in self.config['tools_loaded']) and
|
|
198
|
+
'surfer' in self.config['tools_loaded']) and safe_shutil_which('code'):
|
|
155
199
|
command_list = ['code', '-n', '.', wave_file]
|
|
156
200
|
self.exec(os.path.dirname(wave_file), command_list)
|
|
157
|
-
elif 'gtkwave' in self.config['tools_loaded'] and
|
|
201
|
+
elif 'gtkwave' in self.config['tools_loaded'] and safe_shutil_which('gtkwave'):
|
|
158
202
|
command_list = ['gtkwave', wave_file]
|
|
159
203
|
self.exec(os.path.dirname(wave_file), command_list)
|
|
160
204
|
elif self._vsim_available(from_tools=self.VSIM_VCD_TOOLS):
|
|
@@ -172,7 +216,7 @@ class CommandWaves(CommandDesign):
|
|
|
172
216
|
self, from_tools: list = VSIM_TOOLS
|
|
173
217
|
) -> bool:
|
|
174
218
|
'''Returns True if 'vsim' is available (Questa or Modelsim)'''
|
|
175
|
-
return bool(
|
|
219
|
+
return bool(safe_shutil_which('vsim')) and \
|
|
176
220
|
any(x in self.config['tools_loaded'] for x in from_tools)
|
|
177
221
|
|
|
178
222
|
|