opencos-eda 0.2.48__py3-none-any.whl → 0.2.50__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 +14 -15
- opencos/commands/sim.py +5 -0
- 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 +585 -316
- opencos/eda_config.py +85 -14
- opencos/eda_config_defaults.yml +36 -4
- 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 +7 -4
- opencos/tests/test_eda_synth.py +1 -1
- opencos/tests/test_oc_cli.py +1 -1
- opencos/tests/test_tools.py +4 -2
- opencos/tools/iverilog.py +2 -2
- opencos/tools/modelsim_ase.py +24 -2
- opencos/tools/questa.py +5 -3
- opencos/tools/questa_fse.py +57 -0
- opencos/tools/riviera.py +1 -1
- opencos/tools/slang.py +9 -3
- opencos/tools/surelog.py +1 -1
- opencos/tools/verilator.py +26 -1
- opencos/tools/vivado.py +34 -27
- opencos/tools/yosys.py +4 -3
- opencos/util.py +532 -474
- 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.50.dist-info}/METADATA +1 -1
- opencos_eda-0.2.50.dist-info/RECORD +89 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.50.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.50.dist-info}/WHEEL +0 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.50.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.50.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.50.dist-info}/top_level.txt +0 -0
opencos/util.py
CHANGED
|
@@ -1,27 +1,214 @@
|
|
|
1
|
+
'''opencos.util -- support global logging, argparser for printing (colors)'''
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import sys
|
|
5
|
-
import subprocess
|
|
3
|
+
import argparse
|
|
4
|
+
import atexit
|
|
6
5
|
import datetime
|
|
6
|
+
import json
|
|
7
7
|
import os
|
|
8
|
-
import
|
|
9
|
-
import atexit
|
|
8
|
+
import re
|
|
10
9
|
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
11
13
|
import traceback
|
|
12
|
-
import argparse
|
|
13
|
-
import yaml
|
|
14
|
-
import re
|
|
15
|
-
import textwrap
|
|
16
14
|
|
|
17
|
-
from
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from importlib import import_module
|
|
18
|
+
|
|
19
|
+
global_exit_allowed = False # pylint: disable=invalid-name
|
|
20
|
+
progname = "UNKNOWN" # pylint: disable=invalid-name
|
|
21
|
+
progname_in_message = True # pylint: disable=invalid-name
|
|
22
|
+
debug_level = 0 # pylint: disable=invalid-name
|
|
23
|
+
|
|
24
|
+
args = { # pylint: disable=invalid-name
|
|
25
|
+
'color' : False,
|
|
26
|
+
'quiet' : False,
|
|
27
|
+
'verbose' : False,
|
|
28
|
+
'debug' : False,
|
|
29
|
+
'fancy' : sys.stdout.isatty(),
|
|
30
|
+
'warnings' : 0,
|
|
31
|
+
'errors' : 0,
|
|
32
|
+
'artifacts-json': True,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class Colors:
|
|
36
|
+
'''Namespace class for color printing help'''
|
|
37
|
+
red = "\x1B[31m"
|
|
38
|
+
green = "\x1B[32m"
|
|
39
|
+
orange = "\x1B[33m"
|
|
40
|
+
yellow = "\x1B[39m"
|
|
41
|
+
normal = "\x1B[0m"
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def color_text(text: str, color: str) -> str:
|
|
45
|
+
'''Wraps 'text' (str) with color (one of red|green|orange|yellow) prefix and
|
|
46
|
+
|
|
47
|
+
color (normal) suffix. Disables color prefix/suffix wrapping if args['color']=False
|
|
48
|
+
'''
|
|
49
|
+
if args['color']:
|
|
50
|
+
return color + text + "\x1B[0m" # (normal)
|
|
51
|
+
return text
|
|
52
|
+
|
|
53
|
+
def red_text(text: str) -> str:
|
|
54
|
+
'''Wraps text for printing as red, disabled if global args['color']=False'''
|
|
55
|
+
if args['color']:
|
|
56
|
+
return Colors.red + text + Colors.normal
|
|
57
|
+
return text
|
|
58
|
+
|
|
59
|
+
def green_text(text: str) -> str:
|
|
60
|
+
'''Wraps text for printing as green, disabled if global args['color']=False'''
|
|
61
|
+
if args['color']:
|
|
62
|
+
return Colors.green + text + Colors.normal
|
|
63
|
+
return text
|
|
64
|
+
|
|
65
|
+
def orange_text(text: str) -> str:
|
|
66
|
+
'''Wraps text for printing as orange, disabled if global args['color']=False'''
|
|
67
|
+
if args['color']:
|
|
68
|
+
return Colors.orange + text + Colors.normal
|
|
69
|
+
return text
|
|
70
|
+
|
|
71
|
+
def yellow_text(text: str) -> str:
|
|
72
|
+
'''Wraps text for printing as yellow, disabled if global args['color']=False'''
|
|
73
|
+
if args['color']:
|
|
74
|
+
return Colors.yellow + text + Colors.normal
|
|
75
|
+
return text
|
|
76
|
+
|
|
77
|
+
class ArtifactTypes(Enum):
|
|
78
|
+
'''Types that are allow-listed for artifacts.add* methods. If you don't use one of
|
|
79
|
+
these, you'll get a warning'''
|
|
80
|
+
TEXT = 0
|
|
81
|
+
JSON = 1
|
|
82
|
+
YAML = 2
|
|
83
|
+
WAVEFORM = 3
|
|
84
|
+
DOTF = 4
|
|
85
|
+
TCL = 5
|
|
86
|
+
SHELL = 6
|
|
87
|
+
|
|
88
|
+
class Artifacts:
|
|
89
|
+
'''Class to hold file artifacts, for logs generated by EDA, or other artifcats created
|
|
90
|
+
by a specific tool.
|
|
91
|
+
|
|
92
|
+
If enabled, will write out an artifacts.json file upon util.exit()
|
|
93
|
+
'''
|
|
94
|
+
|
|
95
|
+
data = {}
|
|
96
|
+
unresolved_data = {}
|
|
97
|
+
enabled = True
|
|
98
|
+
artifacts_json_filepath = os.path.join('eda.work', 'artifacts.json')
|
|
99
|
+
|
|
100
|
+
def _get_type_str(self, typ: str) -> (bool, str):
|
|
101
|
+
'''Gets the name from ArtifactTypes, and returns tuple of (True, str)
|
|
102
|
+
|
|
103
|
+
if the ArtifactTypes exists, else (False, str.upper())'''
|
|
104
|
+
ret = getattr(ArtifactTypes, typ.upper(), '')
|
|
105
|
+
if ret:
|
|
106
|
+
return True, ret.name.lower()
|
|
107
|
+
return False, typ.lower()
|
|
108
|
+
|
|
109
|
+
def add(self, name: str, typ: str, description: str = '') -> None:
|
|
110
|
+
'''Adds file to artifacts'''
|
|
111
|
+
|
|
112
|
+
if not self.enabled:
|
|
113
|
+
return # artifacts dumping disabled.
|
|
114
|
+
|
|
115
|
+
_, shortname = os.path.split(name)
|
|
116
|
+
if shortname in self.data:
|
|
117
|
+
return # artifact already registered
|
|
118
|
+
|
|
119
|
+
type_exists, type_str = self._get_type_str(typ)
|
|
120
|
+
if not type_exists:
|
|
121
|
+
warning(f'Unknown artifacts {typ=} add for {name=} {description=}')
|
|
122
|
+
|
|
123
|
+
self.data[shortname] = {
|
|
124
|
+
'name': name,
|
|
125
|
+
'type': type_str,
|
|
126
|
+
'size': 0, # will be filled in when JSON written upon exit.
|
|
127
|
+
'description': description,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def add_extension(
|
|
131
|
+
self, search_paths: list, file_extension: str, typ: str, description: str = ''
|
|
132
|
+
) -> None:
|
|
133
|
+
'''Adds a placeholder for file extensions to add to artifacts
|
|
134
|
+
|
|
135
|
+
For example, when saving a Simulation waves, we may know it's a .fst or .vcd file,
|
|
136
|
+
but we do not know what the testbench SV file has set in $dumpfile(filepath)
|
|
137
|
+
'''
|
|
138
|
+
type_exists, type_str = self._get_type_str(typ)
|
|
139
|
+
if not type_exists:
|
|
140
|
+
warning(f'Unknown artifacts {typ=} add_extension for {search_paths=}',
|
|
141
|
+
f'{file_extension=} {description=}')
|
|
142
|
+
|
|
143
|
+
if isinstance(search_paths, str):
|
|
144
|
+
sp = [search_paths]
|
|
145
|
+
else:
|
|
146
|
+
sp = search_paths
|
|
147
|
+
self.unresolved_data[file_extension] = {
|
|
148
|
+
'search_paths': sp,
|
|
149
|
+
'type': type_str,
|
|
150
|
+
'description': description
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def set_artifacts_json_dir(self, dirpath: str) -> None:
|
|
154
|
+
'''Sets the artifacts_json_filepath'''
|
|
155
|
+
_, right = os.path.split(self.artifacts_json_filepath)
|
|
156
|
+
self.artifacts_json_filepath = os.path.join(dirpath, right)
|
|
157
|
+
|
|
158
|
+
def reset(self, enable: bool = True) -> None:
|
|
159
|
+
'''Clears internal data (dict). Called by util.process_tokens()'''
|
|
160
|
+
self.data = {}
|
|
161
|
+
self.unresolved_data = {}
|
|
162
|
+
self.enabled = enable or args['artifacts-json']
|
|
163
|
+
self.artifacts_json_filepath = os.path.join('eda.work', 'artifacts.json')
|
|
164
|
+
|
|
165
|
+
def _resolve_unresolved_data(self, ext: str) -> None:
|
|
166
|
+
'''Find files for this extension that were registered using add_extension(...)
|
|
167
|
+
|
|
168
|
+
adds to self.data'''
|
|
169
|
+
entry = self.unresolved_data[ext]
|
|
170
|
+
if not entry:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
for search_path in entry['search_paths']:
|
|
174
|
+
p = Path(search_path)
|
|
175
|
+
for posix_filename in p.glob(f'*.{ext}'):
|
|
176
|
+
self.add(
|
|
177
|
+
name=str(posix_filename), typ=entry['type'], description=entry['description']
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def write_json(self) -> None:
|
|
181
|
+
'''Write out the artifacts.json file, called by util.exit()'''
|
|
182
|
+
|
|
183
|
+
if not self.enabled:
|
|
184
|
+
return # artifacts dumping disabled.
|
|
185
|
+
|
|
186
|
+
if self.unresolved_data:
|
|
187
|
+
for ext in self.unresolved_data:
|
|
188
|
+
self._resolve_unresolved_data(ext)
|
|
189
|
+
|
|
190
|
+
if not self.data:
|
|
191
|
+
return # do nothing if we have no artifacts
|
|
192
|
+
|
|
193
|
+
# Update all file sizes:
|
|
194
|
+
for key, entry in self.data.items():
|
|
195
|
+
if os.path.isfile(entry['name']):
|
|
196
|
+
entry['size'] = os.path.getsize(entry['name'])
|
|
197
|
+
else:
|
|
198
|
+
# file doesn't exist, remove it from artifacts.
|
|
199
|
+
warning(f'Removing {key} ({entry["name"]}) from artifacts (file does not exist)')
|
|
200
|
+
del self.data[key]
|
|
201
|
+
|
|
202
|
+
with open(self.artifacts_json_filepath, 'w', encoding='utf-8') as f:
|
|
203
|
+
json.dump(self.data, f, indent=4)
|
|
204
|
+
info(f'Wrote artifacts JSON: {self.artifacts_json_filepath}')
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
artifacts = Artifacts()
|
|
18
208
|
|
|
19
|
-
global_exit_allowed = False
|
|
20
|
-
progname = "UNKNOWN"
|
|
21
|
-
progname_in_message = True
|
|
22
|
-
debug_level = 0
|
|
23
209
|
|
|
24
210
|
class UtilLogger:
|
|
211
|
+
'''Class for the util.global_log'''
|
|
25
212
|
file = None
|
|
26
213
|
filepath = ''
|
|
27
214
|
time_last = 0 #timestamp via time.time()
|
|
@@ -32,11 +219,13 @@ class UtilLogger:
|
|
|
32
219
|
default_log_filepath = os.path.join('eda.work', 'eda.log')
|
|
33
220
|
|
|
34
221
|
def clear(self) -> None:
|
|
222
|
+
'''Resets internals'''
|
|
35
223
|
self.file = None
|
|
36
224
|
self.filepath = ''
|
|
37
225
|
self.time_last = 0
|
|
38
226
|
|
|
39
227
|
def stop(self) -> None:
|
|
228
|
+
'''Closes open log, resets internals'''
|
|
40
229
|
if self.file:
|
|
41
230
|
self.write_timestamp(f'stop - {self.filepath}')
|
|
42
231
|
info(f"Closing logfile: {self.filepath}")
|
|
@@ -44,8 +233,9 @@ class UtilLogger:
|
|
|
44
233
|
self.clear()
|
|
45
234
|
|
|
46
235
|
def start(self, filename: str, force: bool = False) -> None:
|
|
236
|
+
'''Starts (opens) log'''
|
|
47
237
|
if not filename:
|
|
48
|
-
error(
|
|
238
|
+
error('Trying to start a logfile, but filename is missing')
|
|
49
239
|
return
|
|
50
240
|
if os.path.exists(filename):
|
|
51
241
|
if force:
|
|
@@ -57,26 +247,35 @@ class UtilLogger:
|
|
|
57
247
|
else:
|
|
58
248
|
safe_mkdir_for_file(filename)
|
|
59
249
|
try:
|
|
60
|
-
self.file = open(filename, 'w')
|
|
250
|
+
self.file = open(filename, 'w', encoding='utf-8') # pylint: disable=consider-using-with
|
|
61
251
|
debug(f"Opened logfile '{filename}' for writing")
|
|
62
252
|
self.filepath = filename
|
|
63
253
|
self.write_timestamp(f'start - {self.filepath}')
|
|
254
|
+
# add to global artifacts:
|
|
255
|
+
artifacts.add(name=filename, typ='text', description='EDA stdout logfile')
|
|
64
256
|
except Exception as e:
|
|
65
257
|
error(f"Error opening '{filename}' for writing, {e}")
|
|
66
258
|
self.clear()
|
|
67
259
|
|
|
68
260
|
def write_timestamp(self, text: str = "") -> None:
|
|
261
|
+
'''Writes timestamp to opened log'''
|
|
262
|
+
if not self.file:
|
|
263
|
+
return
|
|
69
264
|
dt = datetime.datetime.now().ctime()
|
|
70
265
|
print(f"INFO: [{progname}] Time: {dt} {text}", file=self.file)
|
|
71
266
|
self.time_last = time.time()
|
|
72
267
|
|
|
73
|
-
def write(self, text: str, end: str) -> None:
|
|
74
|
-
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
268
|
+
def write(self, text: str, end: str = '\n') -> None:
|
|
269
|
+
'''Writes text to opened log'''
|
|
270
|
+
if not self.file:
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
if ((time.time() - self.time_last) > 10) and \
|
|
274
|
+
any(text.startswith(x) for x in [
|
|
275
|
+
f"DEBUG: [{progname}]",
|
|
276
|
+
f"INFO: [{progname}]",
|
|
277
|
+
f"WARNING: [{progname}]",
|
|
278
|
+
f"ERROR: [{progname}]"]):
|
|
80
279
|
self.write_timestamp()
|
|
81
280
|
print(text, end=end, file=self.file)
|
|
82
281
|
self.file.flush()
|
|
@@ -86,134 +285,28 @@ class UtilLogger:
|
|
|
86
285
|
global_log = UtilLogger()
|
|
87
286
|
|
|
88
287
|
|
|
89
|
-
def start_log(filename, force=False):
|
|
288
|
+
def start_log(filename: str, force: bool = False) -> None:
|
|
289
|
+
'''Starts the global_log, if not already started'''
|
|
90
290
|
global_log.start(filename=filename, force=force)
|
|
91
291
|
|
|
92
|
-
def write_log(text, end):
|
|
292
|
+
def write_log(text: str, end: str = '\n') -> None:
|
|
293
|
+
'''Writes to the global_log, if started'''
|
|
93
294
|
global_log.write(text=text, end=end)
|
|
94
295
|
|
|
95
|
-
def stop_log():
|
|
296
|
+
def stop_log() -> None:
|
|
297
|
+
'''Stops/closed the global_log'''
|
|
96
298
|
global_log.stop()
|
|
97
299
|
|
|
98
|
-
atexit.register(stop_log)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
EDA_OUTPUT_CONFIG_FNAME = 'eda_output_config.yml'
|
|
102
|
-
|
|
103
|
-
args = {
|
|
104
|
-
'color' : False,
|
|
105
|
-
'quiet' : False,
|
|
106
|
-
'verbose' : False,
|
|
107
|
-
'debug' : False,
|
|
108
|
-
'fancy' : sys.stdout.isatty(),
|
|
109
|
-
'warnings' : 0,
|
|
110
|
-
'errors' : 0,
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
def strip_all_quotes(s: str) -> str:
|
|
114
|
-
return s.replace("'", '').replace('"', '')
|
|
115
|
-
|
|
116
|
-
def strip_outer_quotes(s: str) -> str:
|
|
117
|
-
ret = str(s)
|
|
118
|
-
while (ret.startswith("'") and ret.endswith("'")) or \
|
|
119
|
-
(ret.startswith('"') and ret.endswith('"')):
|
|
120
|
-
ret = ret[1:-1]
|
|
121
|
-
return ret
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def yaml_load_only_root_line_numbers(filepath:str):
|
|
125
|
-
'''Returns a dict of {key: int line number}, very crude'''
|
|
126
|
-
# Other solutions aren't as attractive, require a lot of mappers to get
|
|
127
|
-
# line numbers on returned values that aren't dict
|
|
128
|
-
data = None
|
|
129
|
-
with open(filepath) as f:
|
|
130
|
-
try:
|
|
131
|
-
# Try to do a very lazy parse of root level keys only, returns dict{key:lineno}
|
|
132
|
-
data = dict()
|
|
133
|
-
for lineno,line in enumerate(f.readlines()):
|
|
134
|
-
m = re.match(r'^(\w+):', line)
|
|
135
|
-
if m:
|
|
136
|
-
key = m.group(1)
|
|
137
|
-
data[key] = lineno + 1
|
|
138
|
-
except Exception as e:
|
|
139
|
-
error(f"Error loading YAML {filepath=}:", e)
|
|
140
|
-
return data
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def toml_load_only_root_line_numbers(filepath:str):
|
|
144
|
-
'''Returns a dict of {key: int line number}, very crude'''
|
|
145
|
-
data = None
|
|
146
|
-
with open(filepath) as f:
|
|
147
|
-
try:
|
|
148
|
-
data = dict()
|
|
149
|
-
for lineno, line in enumerate(f.readlines()):
|
|
150
|
-
m = re.match(r'^\[(\w+)\]', line)
|
|
151
|
-
if m:
|
|
152
|
-
key = m.group(1)
|
|
153
|
-
data[key] = lineno + 1
|
|
154
|
-
except Exception as e:
|
|
155
|
-
error(f'Error loading TOML {filepath=}', e)
|
|
156
|
-
return data
|
|
157
300
|
|
|
301
|
+
atexit.register(stop_log)
|
|
158
302
|
|
|
159
|
-
def yaml_safe_load(filepath:str, only_root_line_numbers=False):
|
|
160
|
-
'''Returns dict or None from filepath (str), errors if return type not in assert_return_types.
|
|
161
303
|
|
|
162
|
-
|
|
304
|
+
def get_argparse_bool_action_kwargs() -> dict:
|
|
305
|
+
'''Returns args for BooleanOptionalAction kwargs for an ArgumentParser.add_argument
|
|
163
306
|
|
|
164
|
-
|
|
165
|
-
all the root level keys. Used for debugging DEPS.yml in
|
|
166
|
-
eda.CommandDesign.resolve_target_core
|
|
307
|
+
This is mostly for python compatibility to support --some-enable and --no-some-enable
|
|
167
308
|
'''
|
|
168
|
-
|
|
169
|
-
data = None
|
|
170
|
-
|
|
171
|
-
if only_root_line_numbers:
|
|
172
|
-
return yaml_load_only_root_line_numbers(filepath)
|
|
173
|
-
|
|
174
|
-
with open(filepath) as f:
|
|
175
|
-
debug(f'Opening {filepath=}')
|
|
176
|
-
try:
|
|
177
|
-
data = yaml.safe_load(f)
|
|
178
|
-
except yaml.YAMLError as e:
|
|
179
|
-
|
|
180
|
-
# if yamllint is installed, then use it to get all errors in the .yml|.yaml
|
|
181
|
-
# file, instead of the single exception.
|
|
182
|
-
if shutil.which('yamllint'):
|
|
183
|
-
try:
|
|
184
|
-
sp_out = subprocess.run(
|
|
185
|
-
f'yamllint -d relaxed --no-warnings {filepath}'.split(),
|
|
186
|
-
capture_output=True, text=True )
|
|
187
|
-
for x in sp_out.stdout.split('\n'):
|
|
188
|
-
if x:
|
|
189
|
-
info('yamllint: ' + x)
|
|
190
|
-
except:
|
|
191
|
-
pass
|
|
192
|
-
|
|
193
|
-
if hasattr(e, 'problem_mark'):
|
|
194
|
-
mark = e.problem_mark
|
|
195
|
-
error(f"Error parsing {filepath=}: line {mark.line + 1},",
|
|
196
|
-
f"column {mark.column +1}: {e.problem}")
|
|
197
|
-
else:
|
|
198
|
-
error(f"Error loading YAML {filepath=}:", e)
|
|
199
|
-
except Exception as e:
|
|
200
|
-
error(f"Error loading YAML {filepath=}:", e)
|
|
201
|
-
|
|
202
|
-
return data
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def yaml_safe_writer(data:dict, filepath:str) -> None:
|
|
206
|
-
|
|
207
|
-
if filepath.endswith('.yml') or filepath.endswith('.yaml'):
|
|
208
|
-
with open(filepath, 'w', encoding='utf-8') as f:
|
|
209
|
-
yaml.dump(data, f, allow_unicode=True,
|
|
210
|
-
default_flow_style=False, sort_keys=False, encoding=('utf-8'))
|
|
211
|
-
else:
|
|
212
|
-
warning(f'{filepath=} to be written for this extension not implemented.')
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def get_argparse_bool_action_kwargs() -> dict:
|
|
216
|
-
bool_kwargs = dict()
|
|
309
|
+
bool_kwargs = {}
|
|
217
310
|
x = getattr(argparse, 'BooleanOptionalAction', None)
|
|
218
311
|
if x is not None:
|
|
219
312
|
bool_kwargs['action'] = x
|
|
@@ -221,8 +314,12 @@ def get_argparse_bool_action_kwargs() -> dict:
|
|
|
221
314
|
bool_kwargs['action'] = 'store_true'
|
|
222
315
|
return bool_kwargs
|
|
223
316
|
|
|
317
|
+
|
|
224
318
|
def get_argparser() -> argparse.ArgumentParser:
|
|
225
|
-
|
|
319
|
+
'''Returns the opencos.util ArgumentParser'''
|
|
320
|
+
parser = argparse.ArgumentParser(
|
|
321
|
+
prog='opencos common options', add_help=False, allow_abbrev=False
|
|
322
|
+
)
|
|
226
323
|
# We set allow_abbrev=False so --force-logfile won't try to attempt parsing shorter similarly
|
|
227
324
|
# named args like --force, we want those to go to unparsed list.
|
|
228
325
|
# For bools, support --color and --no-color with this action=argparse.BooleanOptionalAction
|
|
@@ -254,9 +351,13 @@ def get_argparser() -> argparse.ArgumentParser:
|
|
|
254
351
|
parser.add_argument('--no-respawn', action='store_true',
|
|
255
352
|
help=('Legacy mode (default respawn disabled) for respawning eda.py'
|
|
256
353
|
' using $OC_ROOT/bin'))
|
|
354
|
+
parser.add_argument('--artifacts-json', **bool_action_kwargs,
|
|
355
|
+
help='Store a work-dir/artifacts.json file of known tool output files')
|
|
257
356
|
return parser
|
|
258
357
|
|
|
259
|
-
|
|
358
|
+
|
|
359
|
+
def get_argparser_short_help(parser: object = None) -> str:
|
|
360
|
+
'''Returns short help for our ArgumentParser'''
|
|
260
361
|
if not parser:
|
|
261
362
|
parser = get_argparser()
|
|
262
363
|
full_lines = parser.format_help().split('\n')
|
|
@@ -268,18 +369,25 @@ def get_argparser_short_help(parser=None) -> str:
|
|
|
268
369
|
return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
|
|
269
370
|
|
|
270
371
|
|
|
271
|
-
def process_token(arg:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
372
|
+
def process_token(arg: list) -> bool:
|
|
373
|
+
'''Returns true if we parsed arg (list, because we may pass --arg value)
|
|
374
|
+
|
|
375
|
+
This is legacy holdover for oc_cli.py, that would process one token at a time.
|
|
376
|
+
Simply run through our full argparser.
|
|
377
|
+
'''
|
|
378
|
+
_, unparsed = process_tokens(arg)
|
|
379
|
+
if not unparsed: # empty list
|
|
380
|
+
debug(f"Processed token or pair: {arg}")
|
|
277
381
|
return True
|
|
278
382
|
return False
|
|
279
383
|
|
|
280
384
|
|
|
281
385
|
def process_tokens(tokens:list) -> (argparse.Namespace, list):
|
|
282
|
-
|
|
386
|
+
'''Processes tokens (unparsed args list) on util's ArgumentParser
|
|
387
|
+
|
|
388
|
+
Returns tuple of (parsed Namespace, unparsed args list)
|
|
389
|
+
'''
|
|
390
|
+
global debug_level # pylint: disable=global-statement
|
|
283
391
|
|
|
284
392
|
parser = get_argparser()
|
|
285
393
|
try:
|
|
@@ -288,256 +396,328 @@ def process_tokens(tokens:list) -> (argparse.Namespace, list):
|
|
|
288
396
|
except argparse.ArgumentError:
|
|
289
397
|
error(f'problem attempting to parse_known_args for {tokens=}')
|
|
290
398
|
|
|
291
|
-
if parsed.debug_level:
|
|
292
|
-
|
|
293
|
-
|
|
399
|
+
if parsed.debug_level:
|
|
400
|
+
set_debug_level(parsed.debug_level)
|
|
401
|
+
elif parsed.debug:
|
|
402
|
+
set_debug_level(1)
|
|
403
|
+
else:
|
|
404
|
+
debug_level = 0
|
|
294
405
|
|
|
295
406
|
debug(f'util.process_tokens: {parsed=} {unparsed=} from {tokens=}')
|
|
296
407
|
|
|
408
|
+
# clear existing artifacts dicts (mostly for pytests repeatedly calling eda.main),
|
|
409
|
+
# set artifacts.enabled based on args['artifacts-json']
|
|
410
|
+
artifacts.reset(enable=parsed.artifacts_json)
|
|
411
|
+
|
|
297
412
|
if parsed.force_logfile:
|
|
298
413
|
start_log(parsed.force_logfile, force=True)
|
|
299
414
|
elif parsed.logfile:
|
|
300
415
|
start_log(parsed.logfile, force=False)
|
|
301
416
|
elif parsed.default_log and \
|
|
417
|
+
not any(x in unparsed for x in ('help', '-h', '--help')) and \
|
|
302
418
|
(parsed.force_logfile is None and parsed.logfile is None):
|
|
303
419
|
# Use a forced logfile in the eda.work/eda.log:
|
|
420
|
+
# avoid this if someone has --help arg not yet parsed.
|
|
304
421
|
start_log(global_log.default_log_filepath, force=True)
|
|
305
422
|
|
|
306
423
|
|
|
307
424
|
parsed_as_dict = vars(parsed)
|
|
308
425
|
for key,value in parsed_as_dict.items():
|
|
426
|
+
key = key.replace('_', '-')
|
|
309
427
|
if value is not None:
|
|
310
428
|
args[key] = value # only update with non-None values to our global 'args' dict
|
|
311
|
-
return parsed, unparsed
|
|
312
429
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
wrapped_lines = textwrap.wrap(text, width=width,
|
|
316
|
-
initial_indent=' ' * initial_indent,
|
|
317
|
-
subsequent_indent=' ' * indent)
|
|
318
|
-
return '\n'.join(wrapped_lines)
|
|
430
|
+
|
|
431
|
+
return parsed, unparsed
|
|
319
432
|
|
|
320
433
|
# ********************
|
|
321
434
|
# fancy support
|
|
322
|
-
# In fancy mode, we take the bottom
|
|
323
|
-
# while the lines above that show regular scrolling content (via info, debug, warning,
|
|
324
|
-
# User should not use print() when in fancy mode
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def fancy_start(fancy_lines = 4, min_vanilla_lines = 4):
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
435
|
+
# In fancy mode, we take the bottom _FANCY_LINES lines of the screen to be written using
|
|
436
|
+
# fancy_print, while the lines above that show regular scrolling content (via info, debug, warning,
|
|
437
|
+
# error above). User should not use print() when in fancy mode
|
|
438
|
+
|
|
439
|
+
_FANCY_LINES = []
|
|
440
|
+
|
|
441
|
+
def fancy_start(fancy_lines: int = 4, min_vanilla_lines: int = 4) -> None:
|
|
442
|
+
'''Starts fancy line support. This is not called by util internally
|
|
443
|
+
|
|
444
|
+
It is called by an opencos.eda Command handling class, it can check if util.args['fancy']
|
|
445
|
+
is set
|
|
446
|
+
'''
|
|
447
|
+
_, lines = shutil.get_terminal_size()
|
|
448
|
+
if fancy_lines < 2:
|
|
449
|
+
error("Fancy mode requires at least 2 fancy lines")
|
|
450
|
+
if fancy_lines > (lines - min_vanilla_lines):
|
|
451
|
+
error(f"Fancy mode supports at most {(lines - min_vanilla_lines)} fancy lines, given",
|
|
452
|
+
f"{min_vanilla_lines} non-fancy lines")
|
|
453
|
+
if _FANCY_LINES:
|
|
454
|
+
error("We are already in fancy line mode, cannot call fancy_start() again")
|
|
455
|
+
for _ in range(fancy_lines - 1):
|
|
456
|
+
print() # create the requisite number of blank lines
|
|
457
|
+
_FANCY_LINES.append("")
|
|
339
458
|
print("", end="") # the last line has no "\n" because we don't want ANOTHER blank line below
|
|
340
|
-
|
|
459
|
+
_FANCY_LINES.append("")
|
|
341
460
|
# the cursor remains at the leftmost character of the bottom line of the screen
|
|
342
461
|
|
|
343
|
-
def fancy_stop():
|
|
344
|
-
|
|
345
|
-
if
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
462
|
+
def fancy_stop() -> None:
|
|
463
|
+
'''Stops fancy mode. Intended to be called by an opencos.eda Command handling class'''
|
|
464
|
+
if not _FANCY_LINES:
|
|
465
|
+
# don't do anything if we aren't in fancy mode.
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
# user is expected to have painted something into the fancy lines, we can't "pull down"
|
|
469
|
+
# the regular lines above, and we don't want _FANCY_LINES blank or garbage lines either,
|
|
470
|
+
# that's not pretty
|
|
471
|
+
_FANCY_LINES.clear()
|
|
472
|
+
# since cursor is always left at the leftmost character of the bottom line of the screen,
|
|
473
|
+
# which was one of the fancy lines which now has the above-mentioned "something", we want
|
|
474
|
+
# to move one lower
|
|
475
|
+
print()
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def fancy_print(text: str, line: int) -> None:
|
|
479
|
+
'''Fancy print, intended to be called by an opencos.eda Command handling class'''
|
|
480
|
+
|
|
355
481
|
# strip any newline, we don't want to print that
|
|
356
|
-
|
|
357
|
-
lines_above = len(
|
|
482
|
+
text = text.rstrip("\n")
|
|
483
|
+
lines_above = len(_FANCY_LINES) - line - 1
|
|
358
484
|
if lines_above:
|
|
359
|
-
print(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
485
|
+
print(
|
|
486
|
+
(f"\033[{lines_above}A" # move cursor up
|
|
487
|
+
f"{text}\033[1G" # desired text, then move cursor to the first character of the line
|
|
488
|
+
f"\033[{lines_above}B" # move the cursor down
|
|
489
|
+
),
|
|
490
|
+
end="", flush=True
|
|
491
|
+
)
|
|
363
492
|
else:
|
|
364
|
-
print(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
493
|
+
print(
|
|
494
|
+
f"{text}\033[1G", # desired text, then move cursor to the first character of the line
|
|
495
|
+
end="", flush=True
|
|
496
|
+
)
|
|
497
|
+
_FANCY_LINES[line] = text
|
|
498
|
+
|
|
499
|
+
def print_pre() -> None:
|
|
500
|
+
'''called by all util info/warning/debug/error. Handles fancy mode'''
|
|
369
501
|
# stuff we do before printing any line
|
|
370
|
-
if
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
502
|
+
if not _FANCY_LINES:
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
# Also, note that in fancy mode we don't allow the "above lines" to be partially written, they
|
|
506
|
+
# are assumed to be full lines ending in "\n"
|
|
507
|
+
# As always, we expect the cursor was left in the leftmost character of bottom line of screen
|
|
508
|
+
print(
|
|
509
|
+
(f"\033[{len(_FANCY_LINES)-1}A" # move the cursor up to where the first fancy line is drawn
|
|
510
|
+
f"\033[0K" # clear the old fancy line 0
|
|
511
|
+
),
|
|
512
|
+
end="", flush=True
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def print_post(text: str, end: str) -> None:
|
|
516
|
+
'''called by all util info/warning/debug/error. Handles fancy mode'''
|
|
379
517
|
# stuff we do after printing any line
|
|
380
|
-
if
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
print(
|
|
387
|
-
|
|
388
|
-
|
|
518
|
+
if _FANCY_LINES:
|
|
519
|
+
# we just printed a line, including a new line, on top of where fancy line 0 used to be,
|
|
520
|
+
# so cursor is now at the start of fancy line 1. move cursor down to the beginning of the
|
|
521
|
+
# final fancy line (i.e. standard fancy cursor resting place)
|
|
522
|
+
for x, line in enumerate(_FANCY_LINES):
|
|
523
|
+
print("\033[0K", end="") # erase the line to the right
|
|
524
|
+
print(line, flush=True,
|
|
525
|
+
end=('' if x == (len(_FANCY_LINES) - 1) else '\n'))
|
|
526
|
+
|
|
389
527
|
print("\033[1G", end="", flush=True)
|
|
390
|
-
if global_log.file:
|
|
528
|
+
if global_log.file:
|
|
529
|
+
write_log(text, end=end)
|
|
391
530
|
|
|
392
|
-
string_red = f"\x1B[31m"
|
|
393
|
-
string_green = f"\x1B[32m"
|
|
394
|
-
string_orange = f"\x1B[33m"
|
|
395
|
-
string_yellow = f"\x1B[39m"
|
|
396
|
-
string_normal = f"\x1B[0m"
|
|
397
531
|
|
|
398
|
-
def
|
|
532
|
+
def print_color(text: str, color: str, end: str = '\n') -> None:
|
|
533
|
+
'''Note that color(str) must be one of Colors.[red|green|orange|yellow|normal]'''
|
|
399
534
|
print_pre()
|
|
400
|
-
print(
|
|
535
|
+
print(Colors.color_text(text, color), end=end, flush=True)
|
|
401
536
|
print_post(text, end)
|
|
402
537
|
|
|
403
|
-
def
|
|
538
|
+
def print_red(text: str, end: str = '\n') -> None:
|
|
539
|
+
'''Print text as red, goes back to normal color'''
|
|
404
540
|
print_pre()
|
|
405
|
-
print(
|
|
541
|
+
print(red_text(text), end=end, flush=True)
|
|
406
542
|
print_post(text, end)
|
|
407
543
|
|
|
408
|
-
def
|
|
544
|
+
def print_green(text: str, end: str = '\n') -> None:
|
|
545
|
+
'''Print text as green, goes back to normal color'''
|
|
409
546
|
print_pre()
|
|
410
|
-
print(
|
|
547
|
+
print(green_text(text), end=end, flush=True)
|
|
411
548
|
print_post(text, end)
|
|
412
549
|
|
|
413
|
-
def
|
|
550
|
+
def print_orange(text: str, end: str = '\n') -> None:
|
|
551
|
+
'''Print text as orange, goes back to normal color'''
|
|
414
552
|
print_pre()
|
|
415
|
-
print(
|
|
553
|
+
print(orange_text(text), end=end, flush=True)
|
|
416
554
|
print_post(text, end)
|
|
417
555
|
|
|
418
|
-
def
|
|
419
|
-
|
|
556
|
+
def print_yellow(text: str, end: str = '\n') -> None:
|
|
557
|
+
'''Print text as yellow, goes back to normal color'''
|
|
558
|
+
print_pre()
|
|
559
|
+
print(yellow_text(text), end=end, flush=True)
|
|
560
|
+
print_post(text, end)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def set_debug_level(level) -> None:
|
|
564
|
+
'''Sets global debug level, sets args['debug'] and args['verbose']'''
|
|
565
|
+
global debug_level # pylint: disable=global-statement
|
|
420
566
|
debug_level = level
|
|
421
|
-
args['debug'] =
|
|
422
|
-
args['verbose'] =
|
|
567
|
+
args['debug'] = level > 0
|
|
568
|
+
args['verbose'] = level > 1
|
|
423
569
|
info(f"Set debug level to {debug_level}")
|
|
424
570
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
571
|
+
|
|
572
|
+
def debug(*text, level: int = 1, start: object = None, end: str = '\n') -> None:
|
|
573
|
+
'''Print debug messaging (in yellow if possible). If args['debug'] is false, prints nothing.
|
|
574
|
+
|
|
575
|
+
*text: (positional str args) to be printed
|
|
576
|
+
level: (int) debug level to decide if printed or not.
|
|
577
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
578
|
+
end: (optional str) suffix to print
|
|
579
|
+
|
|
580
|
+
Note these messages append to global logging (but require args['debug'] to be set)
|
|
581
|
+
'''
|
|
582
|
+
if start is None:
|
|
583
|
+
start = "DEBUG: " + (f"[{progname}] " if progname_in_message else "")
|
|
584
|
+
if args['debug'] and \
|
|
585
|
+
(((level==1) and args['verbose']) or (debug_level >= level)):
|
|
430
586
|
print_yellow(f"{start}{' '.join(list(text))}", end=end)
|
|
431
587
|
|
|
432
|
-
|
|
433
|
-
|
|
588
|
+
|
|
589
|
+
def info(*text, start: object = None, end='\n') -> None:
|
|
590
|
+
'''Print information messaging (in green if possible). If args['quiet'], prints nothing.
|
|
591
|
+
|
|
592
|
+
*text: (positional str args) to be printed
|
|
593
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
594
|
+
end: (optional str) suffix to print
|
|
595
|
+
|
|
596
|
+
Note these messages append to global logging even if args['quiet'] is set
|
|
597
|
+
'''
|
|
598
|
+
if start is None:
|
|
599
|
+
start = "INFO: " + (f"[{progname}] " if progname_in_message else "")
|
|
434
600
|
if not args['quiet']:
|
|
435
601
|
print_green(f"{start}{' '.join(list(text))}", end=end)
|
|
436
602
|
|
|
437
|
-
def warning(*text, start=
|
|
438
|
-
|
|
603
|
+
def warning(*text, start: object = None, end: str = '\n') -> None:
|
|
604
|
+
'''Print warning messaging (in orange if possible).
|
|
605
|
+
|
|
606
|
+
*text: (positional str args) to be printed
|
|
607
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
608
|
+
end: (optional str) suffix to print
|
|
609
|
+
|
|
610
|
+
Note these messages append to global logging. Increments global args['warnings'] int.
|
|
611
|
+
'''
|
|
612
|
+
if start is None:
|
|
613
|
+
start = "WARNING: " + (f"[{progname}] " if progname_in_message else "")
|
|
439
614
|
args['warnings'] += 1
|
|
440
615
|
print_orange(f"{start}{' '.join(list(text))}", end=end)
|
|
441
616
|
|
|
442
|
-
|
|
443
|
-
|
|
617
|
+
|
|
618
|
+
def error(
|
|
619
|
+
*text, error_code: int = 255, do_exit: bool = True, start: object = None, end: str = '\n'
|
|
620
|
+
) -> int:
|
|
621
|
+
'''Print error messaging (in red if possible).
|
|
622
|
+
|
|
623
|
+
*text: (positional str args) to be printed
|
|
624
|
+
error_code: (int) shell style return code (non-zero is error, but prefer > 1 b/c those are
|
|
625
|
+
python exceptions)
|
|
626
|
+
do_exit: (bool) if True will call exit based on global_exit_allowed.
|
|
627
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
628
|
+
end: (optional str) suffix to print
|
|
629
|
+
|
|
630
|
+
Note these messages append to global logging. Increments global args['errors'] int.
|
|
631
|
+
'''
|
|
632
|
+
if start is None:
|
|
633
|
+
start = "ERROR: " + (f"[{progname}] " if progname_in_message else "")
|
|
444
634
|
args['errors'] += 1
|
|
445
635
|
print_red(f"{start}{' '.join(list(text))}", end=end)
|
|
446
636
|
if do_exit:
|
|
447
|
-
if args['debug']:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
637
|
+
if args['debug']:
|
|
638
|
+
print(traceback.print_stack())
|
|
639
|
+
# Call our overriden-builtin function for 'exit':
|
|
640
|
+
return exit(error_code) # pylint: disable=consider-using-sys-exit
|
|
641
|
+
|
|
642
|
+
if error_code is None:
|
|
643
|
+
return 0
|
|
644
|
+
return abs(int(error_code))
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def exit( # pylint: disable=redefined-builtin
|
|
648
|
+
error_code: int = 0, quiet: bool = False
|
|
649
|
+
) -> int:
|
|
650
|
+
'''sys.exit(int) wrapper, returns the error_code if global_exit_allowed=False'''
|
|
651
|
+
|
|
652
|
+
# Save out artifacts file:
|
|
653
|
+
artifacts.write_json()
|
|
454
654
|
|
|
455
|
-
def exit(error_code=0, quiet=False):
|
|
456
655
|
if global_exit_allowed:
|
|
457
|
-
if not quiet:
|
|
656
|
+
if not quiet:
|
|
657
|
+
info(f"Exiting with {args['warnings']} warnings, {args['errors']} errors")
|
|
458
658
|
sys.exit(error_code)
|
|
459
659
|
|
|
460
660
|
if error_code is None:
|
|
461
661
|
return 0
|
|
462
|
-
|
|
463
|
-
|
|
662
|
+
|
|
663
|
+
return abs(int(error_code))
|
|
664
|
+
|
|
464
665
|
|
|
465
666
|
def getcwd():
|
|
667
|
+
'''Wrapper for os.getcwd() for current working dir'''
|
|
466
668
|
try:
|
|
467
|
-
|
|
669
|
+
return os.getcwd()
|
|
468
670
|
except Exception as e:
|
|
469
|
-
error("Unable to getcwd(), did it get deleted from under us?")
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
global _oc_root_set
|
|
671
|
+
error(f"Unable to getcwd(), did it get deleted from under us? Exception: {e}")
|
|
672
|
+
return None
|
|
673
|
+
|
|
674
|
+
_OC_ROOT = None
|
|
675
|
+
_OC_ROOT_SET = False
|
|
676
|
+
|
|
677
|
+
def get_oc_root(error_on_fail: bool = False) -> None:
|
|
477
678
|
'''Returns a str or False for the root directory of *this* repo.
|
|
478
679
|
|
|
479
680
|
If environment variable OC_ROOT is set, that is used instead, otherwise attempts to use
|
|
480
681
|
`git rev-parse`
|
|
481
682
|
'''
|
|
683
|
+
|
|
684
|
+
global _OC_ROOT # pylint: disable=global-statement
|
|
685
|
+
global _OC_ROOT_SET # pylint: disable=global-statement
|
|
482
686
|
# if we've already run through here once, just return the memorized result
|
|
483
|
-
if
|
|
687
|
+
if _OC_ROOT_SET:
|
|
688
|
+
return _OC_ROOT
|
|
484
689
|
|
|
485
690
|
# try looking for an env var
|
|
486
691
|
s = os.environ.get('OC_ROOT')
|
|
487
692
|
if s:
|
|
488
693
|
debug(f'get_oc_root() -- returning from env: {s=}')
|
|
489
|
-
|
|
694
|
+
_OC_ROOT = s.strip()
|
|
490
695
|
else:
|
|
491
696
|
# try asking GIT
|
|
492
|
-
cp = subprocess.run(
|
|
493
|
-
|
|
697
|
+
cp = subprocess.run(
|
|
698
|
+
'git rev-parse --show-toplevel', stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
699
|
+
shell=True, check=False, universal_newlines=True
|
|
700
|
+
)
|
|
494
701
|
if cp.returncode != 0:
|
|
495
|
-
# TODO(drew): at some point, address the fact that not all repos are oc_root.
|
|
496
|
-
# the repo we are in? or a pointer to the oc_root which
|
|
702
|
+
# TODO(drew): at some point, address the fact that not all repos are oc_root.
|
|
703
|
+
# Is this function asking for the repo we are in? or a pointer to the oc_root which
|
|
704
|
+
# maybe elsewhere on the system?
|
|
497
705
|
print_didnt_find_it = debug
|
|
498
706
|
if error_on_fail:
|
|
499
707
|
print_didnt_find_it = error
|
|
500
|
-
print_didnt_find_it(
|
|
708
|
+
print_didnt_find_it('Unable to get a OC_ROOT directory using git rev-parse')
|
|
501
709
|
else:
|
|
502
|
-
|
|
503
|
-
if sys.platform
|
|
504
|
-
|
|
710
|
+
_OC_ROOT = cp.stdout.strip()
|
|
711
|
+
if sys.platform.startswith('win'):
|
|
712
|
+
_OC_ROOT = _OC_ROOT.replace('/', '\\') # git gives us /, but we need \
|
|
505
713
|
|
|
506
714
|
# there is no sense running through this code more than once
|
|
507
|
-
|
|
508
|
-
return
|
|
715
|
+
_OC_ROOT_SET = True
|
|
716
|
+
return _OC_ROOT
|
|
509
717
|
|
|
510
|
-
def string_or_space(text, whitespace=False):
|
|
511
|
-
if whitespace:
|
|
512
|
-
return " " * len(text)
|
|
513
|
-
else:
|
|
514
|
-
return text
|
|
515
718
|
|
|
516
|
-
def
|
|
517
|
-
|
|
518
|
-
txt = ""
|
|
519
|
-
do_all = False
|
|
520
|
-
# days
|
|
521
|
-
if (s >= (24*60*60)): # greater than 24h, we show days
|
|
522
|
-
d = int(s/(24*60*60))
|
|
523
|
-
txt += f"{d}d:"
|
|
524
|
-
s -= (d*24*60*60)
|
|
525
|
-
do_all = True
|
|
526
|
-
# hours
|
|
527
|
-
if do_all or (s >= (60*60)):
|
|
528
|
-
d = int(s/(60*60))
|
|
529
|
-
txt += f"{d:2}:"
|
|
530
|
-
s -= (d*60*60)
|
|
531
|
-
do_all = True
|
|
532
|
-
# minutes
|
|
533
|
-
d = int(s/(60))
|
|
534
|
-
txt += f"{d:02}:"
|
|
535
|
-
s -= (d*60)
|
|
536
|
-
# seconds
|
|
537
|
-
txt += f"{s:02}"
|
|
538
|
-
return txt
|
|
539
|
-
|
|
540
|
-
def safe_cp(source:str, destination:str, create_dirs:bool=False):
|
|
719
|
+
def safe_cp(source: str, destination: str, create_dirs: bool = False) -> None:
|
|
720
|
+
'''shutil.copy2 wrapper to optionally make the destination directories'''
|
|
541
721
|
try:
|
|
542
722
|
# Infer if destination is a directory
|
|
543
723
|
if destination.endswith('/') or os.path.isdir(destination):
|
|
@@ -551,51 +731,45 @@ def safe_cp(source:str, destination:str, create_dirs:bool=False):
|
|
|
551
731
|
os.makedirs(parent_dir, exist_ok=True)
|
|
552
732
|
# actually copy the file
|
|
553
733
|
shutil.copy2(source, destination)
|
|
734
|
+
info(f"Copied {source} to {destination}")
|
|
554
735
|
except Exception as e:
|
|
555
736
|
print(f"Error copying file from '{source}' to '{destination}': {e}")
|
|
556
|
-
info(f"Copied {source} to {destination}")
|
|
557
737
|
|
|
558
|
-
def safe_rmdir(path):
|
|
559
|
-
"""Safely and reliably remove a non-empty directory."""
|
|
560
|
-
try:
|
|
561
|
-
# Ensure the path exists
|
|
562
|
-
if os.path.exists(path):
|
|
563
|
-
shutil.rmtree(path)
|
|
564
|
-
info(f"Directory '{path}' has been removed successfully.")
|
|
565
|
-
else:
|
|
566
|
-
debug(f"Directory '{path}' does not exist.")
|
|
567
|
-
except Exception as e:
|
|
568
|
-
error(f"An error occurred while removing the directory '{path}': {e}")
|
|
569
738
|
|
|
570
|
-
def safe_mkdir(path : str):
|
|
739
|
+
def safe_mkdir(path : str) -> None:
|
|
740
|
+
'''Attempt to make dir at path, and make all subdirs up to that path'''
|
|
571
741
|
if os.path.exists(path):
|
|
572
742
|
return
|
|
573
|
-
left,
|
|
743
|
+
left, _ = os.path.split(os.path.relpath(path))
|
|
574
744
|
if left and left not in ['.', '..', os.path.sep]:
|
|
575
745
|
safe_mkdir(left)
|
|
576
746
|
try:
|
|
577
747
|
os.mkdir(path)
|
|
578
748
|
except FileExistsError:
|
|
579
749
|
pass
|
|
580
|
-
except:
|
|
750
|
+
except Exception as e1:
|
|
581
751
|
try:
|
|
582
752
|
os.system(f'mkdir -p {path}')
|
|
583
|
-
except Exception as
|
|
584
|
-
error(f'unable to mkdir {path=},
|
|
753
|
+
except Exception as e2:
|
|
754
|
+
error(f'unable to mkdir {path=}, exceptions {e1}, {e2=}')
|
|
755
|
+
|
|
585
756
|
|
|
586
|
-
def safe_mkdirs(base : str, new_dirs : list):
|
|
757
|
+
def safe_mkdirs(base : str, new_dirs : list) -> None:
|
|
758
|
+
'''Create new_dirs at base'''
|
|
587
759
|
for p in new_dirs:
|
|
588
760
|
safe_mkdir( os.path.join(base, p) )
|
|
589
761
|
|
|
590
|
-
|
|
591
|
-
|
|
762
|
+
|
|
763
|
+
def safe_mkdir_for_file(filepath: str) -> None:
|
|
764
|
+
'''Given a new filepath, create dir for that filepath'''
|
|
765
|
+
left, _ = os.path.split(filepath)
|
|
592
766
|
if left:
|
|
593
767
|
safe_mkdir(left)
|
|
594
768
|
|
|
595
769
|
|
|
596
|
-
def import_class_from_string(full_class_name):
|
|
770
|
+
def import_class_from_string(full_class_name: str) -> None:
|
|
597
771
|
"""
|
|
598
|
-
Imports a class given its full name as a
|
|
772
|
+
Imports a class given its full name as a str.
|
|
599
773
|
|
|
600
774
|
Args:
|
|
601
775
|
full_class_name: The full name of the class,
|
|
@@ -604,18 +778,21 @@ def import_class_from_string(full_class_name):
|
|
|
604
778
|
Returns:
|
|
605
779
|
The imported class, or None if an error occurs.
|
|
606
780
|
"""
|
|
607
|
-
from importlib import import_module
|
|
608
781
|
try:
|
|
609
782
|
module_path, class_name = full_class_name.rsplit(".", 1)
|
|
610
783
|
module = import_module(module_path)
|
|
611
784
|
return getattr(module, class_name)
|
|
612
|
-
except
|
|
613
|
-
print(f"Error importing class {full_class_name=}: {e
|
|
785
|
+
except Exception as e:
|
|
786
|
+
print(f"Error importing class {full_class_name=}: {e}")
|
|
614
787
|
return None
|
|
615
788
|
|
|
616
789
|
|
|
617
790
|
class ShellCommandList(list):
|
|
618
|
-
|
|
791
|
+
'''Wrapper around a list, of str that we'll run as a subprocess command
|
|
792
|
+
|
|
793
|
+
included member var for tee_path, to save a log from this subprocess commands list
|
|
794
|
+
'''
|
|
795
|
+
def __init__(self, obj: object = None, tee_fpath: str = ''):
|
|
619
796
|
super().__init__(obj)
|
|
620
797
|
for k in ['tee_fpath']:
|
|
621
798
|
setattr(self, k, getattr(obj, k, None))
|
|
@@ -623,29 +800,35 @@ class ShellCommandList(list):
|
|
|
623
800
|
self.tee_fpath = tee_fpath
|
|
624
801
|
|
|
625
802
|
|
|
626
|
-
def write_shell_command_file(
|
|
627
|
-
|
|
803
|
+
def write_shell_command_file(
|
|
804
|
+
dirpath : str, filename : str, command_lists : list, line_breaks : bool = False
|
|
805
|
+
) -> None:
|
|
806
|
+
''' Writes new file at {dirpath}/{filename} as a bash shell command, using command_lists
|
|
807
|
+
(list of lists)
|
|
628
808
|
|
|
629
809
|
-- dirpath (str) -- directory where file is written (usually eda.work/{target}_sim
|
|
630
810
|
-- filename (str) -- filename, for example compile_only.sh
|
|
631
|
-
-- command_lists (list) -- list of (list or ShellCommandList), each item in the list is a
|
|
632
|
-
subprocess.run(args) uses a list of
|
|
633
|
-
|
|
634
|
-
|
|
811
|
+
-- command_lists (list) -- list of (list or ShellCommandList), each item in the list is a
|
|
812
|
+
list of commands (aka, how subprocess.run(args) uses a list of
|
|
813
|
+
commands.
|
|
814
|
+
-- line_breaks (bool) -- Set to True to have 1 word per line in the file followed by a line
|
|
815
|
+
break. Default False has an entry in command_lists all on a single
|
|
816
|
+
line.
|
|
635
817
|
|
|
636
818
|
Returns None, writes the file and chmod's it to 0x755.
|
|
637
|
-
|
|
638
819
|
'''
|
|
820
|
+
|
|
639
821
|
# command_lists should be a list-of-lists.
|
|
640
822
|
bash_path = shutil.which('bash')
|
|
641
|
-
assert
|
|
823
|
+
assert isinstance(command_lists, list), f'{command_lists=}'
|
|
642
824
|
fullpath = os.path.join(dirpath, filename)
|
|
643
|
-
with open(fullpath, 'w') as f:
|
|
825
|
+
with open(fullpath, 'w', encoding='utf-8') as f:
|
|
644
826
|
if not bash_path:
|
|
645
827
|
bash_path = "/bin/bash" # we may not get far, but we'll try
|
|
646
828
|
f.write('#!' + bash_path + '\n\n')
|
|
647
829
|
for obj in command_lists:
|
|
648
|
-
assert isinstance(obj, list),
|
|
830
|
+
assert isinstance(obj, list), \
|
|
831
|
+
f'{obj=} (obj must be list/ShellCommandList) {command_lists=}'
|
|
649
832
|
clist = list(obj).copy()
|
|
650
833
|
tee_fpath = getattr(obj, 'tee_fpath', None)
|
|
651
834
|
if tee_fpath:
|
|
@@ -657,7 +840,7 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
|
|
|
657
840
|
else:
|
|
658
841
|
clist.append(f'2>&1 | tee {tee_fpath}')
|
|
659
842
|
|
|
660
|
-
if
|
|
843
|
+
if clist:
|
|
661
844
|
if line_breaks:
|
|
662
845
|
# line_breaks=True - have 1 word per line, followed by \ and \n
|
|
663
846
|
sep = " \\" + "\n"
|
|
@@ -674,26 +857,6 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
|
|
|
674
857
|
os.chmod(fullpath, 0o755)
|
|
675
858
|
|
|
676
859
|
|
|
677
|
-
def write_eda_config_and_args(dirpath : str, filename=EDA_OUTPUT_CONFIG_FNAME, command_obj_ref=None):
|
|
678
|
-
import copy
|
|
679
|
-
if command_obj_ref is None:
|
|
680
|
-
return
|
|
681
|
-
fullpath = os.path.join(dirpath, filename)
|
|
682
|
-
data = dict()
|
|
683
|
-
for x in ['command_name', 'config', 'target', 'args', 'modified_args', 'defines',
|
|
684
|
-
'incdirs', 'files_v', 'files_sv', 'files_vhd']:
|
|
685
|
-
# Use deep copy b/c otherwise these are references to opencos.eda.
|
|
686
|
-
data[x] = copy.deepcopy(getattr(command_obj_ref, x, ''))
|
|
687
|
-
|
|
688
|
-
# fix some burried class references in command_obj_ref.config,
|
|
689
|
-
# otherwise we won't be able to safe load this yaml, so cast as str repr.
|
|
690
|
-
for k, v in command_obj_ref.config.items():
|
|
691
|
-
if k == 'command_handler':
|
|
692
|
-
data['config'][k] = str(v)
|
|
693
|
-
|
|
694
|
-
yaml_safe_writer(data=data, filepath=fullpath)
|
|
695
|
-
|
|
696
|
-
|
|
697
860
|
def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
|
|
698
861
|
'''Returns the best guess as the 'top' module name name, given a fpath where
|
|
699
862
|
|
|
@@ -718,114 +881,9 @@ def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
|
|
|
718
881
|
if bool(re.fullmatch(r'^\w+$', module_name)):
|
|
719
882
|
if module_name == module_guess:
|
|
720
883
|
return module_guess
|
|
721
|
-
|
|
884
|
+
if module_name:
|
|
722
885
|
best_guess = module_name
|
|
723
886
|
if best_guess:
|
|
724
887
|
return best_guess
|
|
725
|
-
else:
|
|
726
|
-
return ''
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
def subprocess_run(work_dir, command_list, fake:bool=False, shell=False) -> int:
|
|
730
|
-
''' Run command_list in the foreground, with preference to use bash if shell=True.'''
|
|
731
|
-
|
|
732
|
-
if work_dir is not None:
|
|
733
|
-
os.chdir(work_dir)
|
|
734
888
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
proc_kwargs = {'shell': shell}
|
|
738
|
-
bash_exec = shutil.which('bash')
|
|
739
|
-
if shell and bash_exec and not is_windows:
|
|
740
|
-
proc_kwargs.update({'executable': bash_exec})
|
|
741
|
-
|
|
742
|
-
if not is_windows and shell:
|
|
743
|
-
c = ' '.join(command_list)
|
|
744
|
-
else:
|
|
745
|
-
c = command_list
|
|
746
|
-
|
|
747
|
-
if fake:
|
|
748
|
-
info(f"util.subprocess_run FAKE: would have called subprocess.run({c}, **{proc_kwargs}")
|
|
749
|
-
return 0
|
|
750
|
-
else:
|
|
751
|
-
debug(f"util.subprocess_run: About to call subprocess.run({c}, **{proc_kwargs}")
|
|
752
|
-
proc = subprocess.run(c, **proc_kwargs)
|
|
753
|
-
return proc.returncode
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
def subprocess_run_background(work_dir, command_list, background=True, fake:bool=False,
|
|
757
|
-
shell=False, tee_fpath=None) -> (str, str, int):
|
|
758
|
-
''' Run command_list in the background, with preference to use bash if shell=True
|
|
759
|
-
|
|
760
|
-
tee_fpath is relative to work_dir.
|
|
761
|
-
'''
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
is_windows = sys.platform.startswith('win')
|
|
765
|
-
|
|
766
|
-
debug(f'util.subprocess_run_background: {background=} {tee_fpath=} {shell=}')
|
|
767
|
-
|
|
768
|
-
if fake:
|
|
769
|
-
# let subprocess_run handle it (won't run anything)
|
|
770
|
-
rc = subprocess_run(work_dir, command_list, fake=fake, shell=shell)
|
|
771
|
-
return '', '', rc
|
|
772
|
-
|
|
773
|
-
if work_dir is not None:
|
|
774
|
-
os.chdir(work_dir)
|
|
775
|
-
|
|
776
|
-
proc_kwargs = {'shell': shell,
|
|
777
|
-
'stdout': subprocess.PIPE,
|
|
778
|
-
'stderr': subprocess.STDOUT,
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
bash_exec = shutil.which('bash')
|
|
782
|
-
if shell and bash_exec and not is_windows:
|
|
783
|
-
# Note - windows powershell will end up calling: /bin/bash /c, which won't work
|
|
784
|
-
proc_kwargs.update({'executable': bash_exec})
|
|
785
|
-
|
|
786
|
-
if not is_windows and shell:
|
|
787
|
-
c = ' '.join(command_list)
|
|
788
|
-
else:
|
|
789
|
-
c = command_list # leave as list.
|
|
790
|
-
|
|
791
|
-
debug(f"util.subprocess_run_background: about to call subprocess.Popen({c}, **{proc_kwargs})")
|
|
792
|
-
proc = subprocess.Popen(c, **proc_kwargs)
|
|
793
|
-
|
|
794
|
-
stdout = ''
|
|
795
|
-
stderr = ''
|
|
796
|
-
tee_fpath_f = None
|
|
797
|
-
if tee_fpath:
|
|
798
|
-
try:
|
|
799
|
-
tee_fpath_f = open(tee_fpath, 'w')
|
|
800
|
-
except Exception as e:
|
|
801
|
-
error(f'Unable to open file "{tee_fpath}" for writing, {e}')
|
|
802
|
-
|
|
803
|
-
for line in iter(proc.stdout.readline, b''):
|
|
804
|
-
line = line.rstrip().decode("utf-8", errors="replace")
|
|
805
|
-
if not background:
|
|
806
|
-
print(line)
|
|
807
|
-
if tee_fpath_f:
|
|
808
|
-
tee_fpath_f.write(line + '\n')
|
|
809
|
-
if global_log.file:
|
|
810
|
-
global_log.write(line, '\n')
|
|
811
|
-
stdout += line + '\n'
|
|
812
|
-
|
|
813
|
-
proc.communicate()
|
|
814
|
-
rc = proc.returncode
|
|
815
|
-
if tee_fpath_f:
|
|
816
|
-
tee_fpath_f.write(f'INFO: [{progname}] util.subprocess_run_background: returncode={rc}\n')
|
|
817
|
-
tee_fpath_f.close()
|
|
818
|
-
info('util.subprocess_run_background: wrote: ' + os.path.abspath(tee_fpath))
|
|
819
|
-
|
|
820
|
-
return stdout, stderr, rc
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
def sanitize_defines_for_sh(value):
|
|
824
|
-
# Need to sanitize this for shell in case someone sends a +define+foo+1'b0,
|
|
825
|
-
# which needs to be escaped as +define+foo+1\'b0, otherwise bash or sh will
|
|
826
|
-
# think this is an unterminated string.
|
|
827
|
-
# TODO(drew): decide if we should instead us shlex.quote('+define+key=value')
|
|
828
|
-
# instead of this function.
|
|
829
|
-
if type(value) is str:
|
|
830
|
-
value = value.replace("'", "\\" + "'")
|
|
831
|
-
return value
|
|
889
|
+
return ''
|