opencos-eda 0.2.48__py3-none-any.whl → 0.2.49__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/__init__.py +4 -2
- opencos/_version.py +10 -7
- opencos/commands/flist.py +8 -7
- opencos/commands/multi.py +13 -14
- opencos/commands/sweep.py +3 -2
- opencos/deps/__init__.py +0 -0
- opencos/deps/defaults.py +69 -0
- opencos/deps/deps_commands.py +419 -0
- opencos/deps/deps_file.py +326 -0
- opencos/deps/deps_processor.py +670 -0
- opencos/deps_schema.py +7 -8
- opencos/eda.py +84 -64
- opencos/eda_base.py +572 -316
- opencos/eda_config.py +80 -14
- opencos/eda_extract_targets.py +22 -14
- opencos/eda_tool_helper.py +33 -7
- opencos/export_helper.py +166 -86
- opencos/export_json_convert.py +31 -23
- opencos/files.py +2 -1
- opencos/hw/__init__.py +0 -0
- opencos/{oc_cli.py → hw/oc_cli.py} +9 -4
- opencos/names.py +0 -4
- opencos/peakrdl_cleanup.py +13 -7
- opencos/seed.py +19 -11
- opencos/tests/helpers.py +3 -2
- opencos/tests/test_deps_helpers.py +35 -32
- opencos/tests/test_eda.py +36 -29
- opencos/tests/test_eda_elab.py +5 -3
- opencos/tests/test_eda_synth.py +1 -1
- opencos/tests/test_oc_cli.py +1 -1
- opencos/tests/test_tools.py +3 -2
- opencos/tools/iverilog.py +2 -2
- opencos/tools/modelsim_ase.py +2 -2
- opencos/tools/riviera.py +1 -1
- opencos/tools/slang.py +1 -1
- opencos/tools/surelog.py +1 -1
- opencos/tools/verilator.py +1 -1
- opencos/tools/vivado.py +1 -1
- opencos/tools/yosys.py +4 -3
- opencos/util.py +374 -468
- opencos/utils/__init__.py +0 -0
- opencos/utils/markup_helpers.py +98 -0
- opencos/utils/str_helpers.py +111 -0
- opencos/utils/subprocess_helpers.py +108 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/METADATA +1 -1
- opencos_eda-0.2.49.dist-info/RECORD +88 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/entry_points.txt +1 -1
- opencos/deps_helpers.py +0 -1346
- opencos_eda-0.2.48.dist-info/RECORD +0 -79
- /opencos/{pcie.py → hw/pcie.py} +0 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/WHEEL +0 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'''deps_file -- functions and DepsFile class (for holding info and getting data from a
|
|
2
|
+
DEPS markup file.)
|
|
3
|
+
|
|
4
|
+
Performs no procesing.
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import toml
|
|
12
|
+
|
|
13
|
+
from opencos.deps.defaults import DEPS_FILE_EXTS, ROOT_TABLE_KEYS_NOT_TARGETS
|
|
14
|
+
from opencos.util import debug, error
|
|
15
|
+
from opencos.utils.markup_helpers import yaml_safe_load, toml_load_only_root_line_numbers
|
|
16
|
+
from opencos.utils.str_helpers import fnmatch_or_re, dep_str2list
|
|
17
|
+
from opencos.utils.subprocess_helpers import subprocess_run_background
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def deps_data_get_all_targets(data: dict) -> list:
|
|
21
|
+
'''Given extracted DEPS data (dict) get all the root level keys that aren't defaults'''
|
|
22
|
+
return [x for x in data.keys() if x not in ROOT_TABLE_KEYS_NOT_TARGETS]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_deps_markup_file(base_path: str) -> str:
|
|
26
|
+
'''Returns one of DEPS.yml, DEPS.yaml, DEPS.toml, DEPS.json'''
|
|
27
|
+
for suffix in DEPS_FILE_EXTS:
|
|
28
|
+
deps_file = os.path.join(base_path, 'DEPS' + suffix)
|
|
29
|
+
if os.path.isfile(deps_file):
|
|
30
|
+
return deps_file
|
|
31
|
+
return ''
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def deps_markup_safe_load(
|
|
35
|
+
filepath: str, assert_return_types: tuple = (type(None), dict),
|
|
36
|
+
only_root_line_numbers: bool = False
|
|
37
|
+
) -> dict:
|
|
38
|
+
'''Returns dict (may return {}) from filepath (str), errors if return type not in
|
|
39
|
+
assert_return_types.
|
|
40
|
+
|
|
41
|
+
(assert_return_types can be empty tuple, or any type to avoid check.)
|
|
42
|
+
|
|
43
|
+
only_root_line_numbers -- if True, will return a dict of {key: line number (int)} for
|
|
44
|
+
all the root level keys. Used for debugging DEPS.yml in
|
|
45
|
+
eda.CommandDesign.resolve_target_core
|
|
46
|
+
'''
|
|
47
|
+
data = {}
|
|
48
|
+
_, file_ext = os.path.splitext(filepath)
|
|
49
|
+
if file_ext in ['', '.yml', 'yaml']:
|
|
50
|
+
# treat DEPS as YAML.
|
|
51
|
+
data = yaml_safe_load(filepath=filepath, only_root_line_numbers=only_root_line_numbers)
|
|
52
|
+
elif file_ext == '.toml':
|
|
53
|
+
if only_root_line_numbers:
|
|
54
|
+
data = toml_load_only_root_line_numbers(filepath)
|
|
55
|
+
else:
|
|
56
|
+
data = toml.load(filepath)
|
|
57
|
+
elif file_ext == '.json':
|
|
58
|
+
if only_root_line_numbers:
|
|
59
|
+
data = {}
|
|
60
|
+
else:
|
|
61
|
+
with open(filepath, encoding='utf=8') as f:
|
|
62
|
+
data = json.load(f)
|
|
63
|
+
|
|
64
|
+
if assert_return_types and not isinstance(data, assert_return_types):
|
|
65
|
+
error(f'deps_markeup_safe_load: {filepath=} loaded type {type(data)=} is not in',
|
|
66
|
+
f'{assert_return_types=}')
|
|
67
|
+
|
|
68
|
+
return data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_all_targets( # pylint: disable=dangerous-default-value,too-many-locals,too-many-branches
|
|
72
|
+
dirs: list = [os.getcwd()],
|
|
73
|
+
base_path: str = os.getcwd(),
|
|
74
|
+
filter_str: str = '',
|
|
75
|
+
filter_using_multi: str = '',
|
|
76
|
+
error_on_empty_return: bool = True,
|
|
77
|
+
lstrip_path: bool = True
|
|
78
|
+
) -> list:
|
|
79
|
+
'''Returns a list of [dir/target, ... ] using relpath from base_path
|
|
80
|
+
|
|
81
|
+
If using filter_using_multi (str), dirs (list) is not required. Example:
|
|
82
|
+
filter_using_multi='sim --tool vivado path/to/*test'
|
|
83
|
+
and filter_str is applied to all resulting targets.
|
|
84
|
+
|
|
85
|
+
If not using filter_using_multi, dirs is required, and filter_str is applied
|
|
86
|
+
To all targets from dirs.
|
|
87
|
+
'''
|
|
88
|
+
|
|
89
|
+
_path_lprefix = str(Path('.')) + os.path.sep
|
|
90
|
+
|
|
91
|
+
if filter_using_multi:
|
|
92
|
+
targets = []
|
|
93
|
+
orig_dir = os.path.abspath(os.getcwd())
|
|
94
|
+
os.chdir(base_path)
|
|
95
|
+
cmd_str = 'eda multi --quiet --print-targets ' + filter_using_multi
|
|
96
|
+
stdout, _, rc = subprocess_run_background(
|
|
97
|
+
work_dir='.', command_list=cmd_str.split()
|
|
98
|
+
)
|
|
99
|
+
os.chdir(orig_dir)
|
|
100
|
+
if rc != 0:
|
|
101
|
+
error(f'get_all_targets: {base_path=} {filter_using_multi=} {cmd_str=} returned:',
|
|
102
|
+
f'{rc=}, {stdout=}')
|
|
103
|
+
|
|
104
|
+
multi_filtered_targets = stdout.split()
|
|
105
|
+
if not filter_str:
|
|
106
|
+
targets = multi_filtered_targets
|
|
107
|
+
else:
|
|
108
|
+
targets = set()
|
|
109
|
+
for target in multi_filtered_targets:
|
|
110
|
+
this_dir, leaf_target = os.path.split(target)
|
|
111
|
+
if fnmatch_or_re(pattern=filter_str,
|
|
112
|
+
string=leaf_target):
|
|
113
|
+
t = os.path.join(os.path.relpath(this_dir, start=base_path), leaf_target)
|
|
114
|
+
if lstrip_path:
|
|
115
|
+
t = t.removeprefix(_path_lprefix)
|
|
116
|
+
targets.add(t)
|
|
117
|
+
targets = list(targets)
|
|
118
|
+
if not targets and error_on_empty_return:
|
|
119
|
+
error(f'get_all_targets: {base_path=} {filter_using_multi=} returned no targets')
|
|
120
|
+
return targets
|
|
121
|
+
|
|
122
|
+
targets = set()
|
|
123
|
+
for this_dir in dirs:
|
|
124
|
+
this_dir = os.path.join(base_path, this_dir)
|
|
125
|
+
deps_file = get_deps_markup_file(this_dir)
|
|
126
|
+
if not deps_file:
|
|
127
|
+
continue
|
|
128
|
+
data = deps_markup_safe_load(filepath=deps_file)
|
|
129
|
+
|
|
130
|
+
for leaf_target in deps_data_get_all_targets(data):
|
|
131
|
+
if not filter_str or fnmatch_or_re(pattern=filter_str,
|
|
132
|
+
string=leaf_target):
|
|
133
|
+
t = os.path.join(os.path.relpath(this_dir, start=base_path), leaf_target)
|
|
134
|
+
if lstrip_path:
|
|
135
|
+
t = t.removeprefix(_path_lprefix)
|
|
136
|
+
targets.add(t)
|
|
137
|
+
|
|
138
|
+
if not targets and error_on_empty_return:
|
|
139
|
+
error(f'get_all_targets: {base_path=} {dirs=} {filter_str=} returned no targets')
|
|
140
|
+
return list(targets)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def deps_target_get_deps_list(
|
|
144
|
+
entry, default_key: str = 'deps', target_node: str = '',
|
|
145
|
+
deps_file: str = '', entry_must_have_default_key: bool = False
|
|
146
|
+
) -> list:
|
|
147
|
+
'''Given a DEPS table entry (str, list, dict) return the 'deps:' list'''
|
|
148
|
+
|
|
149
|
+
# For convenience, if key 'deps' in not in an entry, and entry is a list or string, then
|
|
150
|
+
# assume it's a list of deps
|
|
151
|
+
debug(f'{deps_file=} {target_node=}: {entry=} {default_key=}')
|
|
152
|
+
deps = []
|
|
153
|
+
if isinstance(entry, str):
|
|
154
|
+
deps = dep_str2list(entry)
|
|
155
|
+
elif isinstance(entry, list):
|
|
156
|
+
deps = entry # already a list
|
|
157
|
+
elif isinstance(entry, dict):
|
|
158
|
+
|
|
159
|
+
if entry_must_have_default_key:
|
|
160
|
+
assert default_key in entry, \
|
|
161
|
+
f'{target_node=} in {deps_file=} does not have a key for {default_key=} in {entry=}'
|
|
162
|
+
deps = entry.get(default_key, [])
|
|
163
|
+
deps = dep_str2list(deps)
|
|
164
|
+
|
|
165
|
+
# Strip commented out list entries, strip blank strings, preserve non-strings
|
|
166
|
+
ret = []
|
|
167
|
+
for dep in deps:
|
|
168
|
+
if isinstance(dep, str):
|
|
169
|
+
if dep.startswith('#') or dep == '':
|
|
170
|
+
continue
|
|
171
|
+
ret.append(dep)
|
|
172
|
+
return ret
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def deps_list_target_sanitize(
|
|
176
|
+
entry, default_key: str = 'deps', target_node: str = '', deps_file: str = ''
|
|
177
|
+
) -> dict:
|
|
178
|
+
'''Returns a sanitized DEPS markup table entry (dict --> dict)
|
|
179
|
+
|
|
180
|
+
Since we support target entries that can be dict, list, or str(), sanitize
|
|
181
|
+
them so they are a dict, with a key named 'deps' that has a list of deps.
|
|
182
|
+
'''
|
|
183
|
+
if isinstance(entry, dict):
|
|
184
|
+
return entry
|
|
185
|
+
|
|
186
|
+
if isinstance(entry, str):
|
|
187
|
+
mylist = dep_str2list(entry) # convert str to list
|
|
188
|
+
return {default_key: mylist}
|
|
189
|
+
|
|
190
|
+
if isinstance(entry, list):
|
|
191
|
+
# it's already a list
|
|
192
|
+
return {default_key: entry}
|
|
193
|
+
|
|
194
|
+
assert False, f"Can't convert to list {entry=} {default_key=} {target_node=} {deps_file=}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class DepsFile:
|
|
198
|
+
'''A Container for a DEPS.yml or other Markup file
|
|
199
|
+
|
|
200
|
+
References the original CommandDesign object and its cache
|
|
201
|
+
|
|
202
|
+
Used for looking up a target, getting its line number in the original file, and
|
|
203
|
+
merging contents with a DEFAULTS key if present.
|
|
204
|
+
'''
|
|
205
|
+
|
|
206
|
+
def __init__( # pylint: disable=dangerous-default-value
|
|
207
|
+
self, command_design_ref: object, target_path: str, cache: dict = {}
|
|
208
|
+
):
|
|
209
|
+
self.target_path = target_path
|
|
210
|
+
self.deps_file = get_deps_markup_file(target_path)
|
|
211
|
+
self.rel_deps_file = self.deps_file
|
|
212
|
+
|
|
213
|
+
if not self.deps_file:
|
|
214
|
+
# didn't find it, file doesn't exist.
|
|
215
|
+
self.data = {}
|
|
216
|
+
self.line_numbers = {}
|
|
217
|
+
elif self.deps_file in cache:
|
|
218
|
+
self.data = cache[self.deps_file].get('data', {})
|
|
219
|
+
self.line_numbers = cache[self.deps_file].get('line_numbers', {})
|
|
220
|
+
else:
|
|
221
|
+
self.data = deps_markup_safe_load(self.deps_file)
|
|
222
|
+
self.line_numbers = deps_markup_safe_load(self.deps_file, only_root_line_numbers=True)
|
|
223
|
+
cache[self.deps_file] = {
|
|
224
|
+
'data': self.data,
|
|
225
|
+
'line_numbers': self.line_numbers,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if self.deps_file:
|
|
229
|
+
deps_path, deps_leaf = os.path.split(self.deps_file)
|
|
230
|
+
if deps_path and os.path.exists(deps_path):
|
|
231
|
+
self.rel_deps_file = os.path.join(os.path.relpath(deps_path), deps_leaf)
|
|
232
|
+
|
|
233
|
+
self.error = command_design_ref.error # method.
|
|
234
|
+
|
|
235
|
+
def found(self) -> bool:
|
|
236
|
+
'''Returns true if this DEPS file exists and extracted non-empty data'''
|
|
237
|
+
return bool(self.deps_file) and bool(self.data)
|
|
238
|
+
|
|
239
|
+
def get_approx_line_number_str(self, target) -> str:
|
|
240
|
+
'''Given a full target name, get the approximate line numbers in the DEPS file if
|
|
241
|
+
available in self.line_numbers'''
|
|
242
|
+
_, target_node = os.path.split(target)
|
|
243
|
+
if not self.line_numbers:
|
|
244
|
+
return ''
|
|
245
|
+
|
|
246
|
+
return f'line={self.line_numbers.get(target_node, "")}'
|
|
247
|
+
|
|
248
|
+
def gen_caller_info(self, target: str) -> str:
|
|
249
|
+
'''Given a full target name (path/to/my_target) return caller_info str for debug'''
|
|
250
|
+
return '::'.join([
|
|
251
|
+
self.rel_deps_file,
|
|
252
|
+
target,
|
|
253
|
+
self.get_approx_line_number_str(target)
|
|
254
|
+
])
|
|
255
|
+
|
|
256
|
+
def lookup(self, target_node: str, caller_info: str) -> bool:
|
|
257
|
+
'''Returns True if the target_node is in the DEPS markup file. If not, error with
|
|
258
|
+
|
|
259
|
+
some caller_info(str). This is more useful for YAML or TOML markup where we have
|
|
260
|
+
caller_info.
|
|
261
|
+
'''
|
|
262
|
+
if target_node not in self.data:
|
|
263
|
+
found_target = False
|
|
264
|
+
# For error printing, prefer relative paths:
|
|
265
|
+
t_path = os.path.relpath(self.target_path) + os.path.sep
|
|
266
|
+
t_node = target_node
|
|
267
|
+
t_full = os.path.join(t_path, t_node)
|
|
268
|
+
if not caller_info:
|
|
269
|
+
# If we don't have caller_info, likely came from command line (or DEPS JSON data):
|
|
270
|
+
if '.' in target_node:
|
|
271
|
+
# Likely a filename (target_node does not include path)
|
|
272
|
+
self.error(f'Trying to resolve command-line target={t_full} (file?):',
|
|
273
|
+
f'File={t_node} not found in directory={t_path}')
|
|
274
|
+
elif not self.rel_deps_file:
|
|
275
|
+
# target, but there's no DEPS file
|
|
276
|
+
self.error(f'Trying to resolve command-line target={t_full}:',
|
|
277
|
+
f'but path {t_path} has no DEPS markup file (DEPS.yml)')
|
|
278
|
+
else:
|
|
279
|
+
self.error(f'Trying to resolve command-line target={t_full}:',
|
|
280
|
+
f'was not found in deps_file={self.rel_deps_file},',
|
|
281
|
+
f'possible targets in deps file = {list(self.data.keys())}')
|
|
282
|
+
else:
|
|
283
|
+
# If we have caller_info, then this was a recursive call from another
|
|
284
|
+
# DEPS file. It should already have the useful error messaging:
|
|
285
|
+
|
|
286
|
+
if '.' in target_node:
|
|
287
|
+
# Likely a filename (target_node does not include path)
|
|
288
|
+
self.error(f'Trying to resolve target={t_full} (file?):',
|
|
289
|
+
f'called from {caller_info},',
|
|
290
|
+
f'File={t_node} not found in directory={t_path}')
|
|
291
|
+
elif not self.rel_deps_file:
|
|
292
|
+
# target, but there's no DEPS file
|
|
293
|
+
self.error(f'Trying to resolve target={t_full}:',
|
|
294
|
+
f'called from {caller_info},',
|
|
295
|
+
f'but {t_path} has no DEPS markup file (DEPS.yml)')
|
|
296
|
+
else:
|
|
297
|
+
self.error(f'Trying to resolve target={t_full}:',
|
|
298
|
+
f'called from {caller_info},',
|
|
299
|
+
f'Target not found in deps_file={self.rel_deps_file}')
|
|
300
|
+
else:
|
|
301
|
+
debug(f'Found {target_node=} in deps_file={self.rel_deps_file}')
|
|
302
|
+
found_target = True
|
|
303
|
+
|
|
304
|
+
return found_target
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_entry(self, target_node) -> dict:
|
|
308
|
+
'''Returns the DEPS markup table "entry" (dict) for a target
|
|
309
|
+
|
|
310
|
+
This has DEFAULTS applied, and is converted to a dict if it wasn't already
|
|
311
|
+
'''
|
|
312
|
+
|
|
313
|
+
# Start with the defaults
|
|
314
|
+
entry = self.data.get('DEFAULTS', {}).copy()
|
|
315
|
+
|
|
316
|
+
# Lookup the entry from the DEPS dict:
|
|
317
|
+
entry_raw = self.data[target_node]
|
|
318
|
+
|
|
319
|
+
entry_sanitized = deps_list_target_sanitize(
|
|
320
|
+
entry_raw, target_node=target_node, deps_file=self.deps_file
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Finally update entry (defaults) with what we looked up:
|
|
324
|
+
entry.update(entry_sanitized)
|
|
325
|
+
|
|
326
|
+
return entry
|