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
opencos/util.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
# SPDX-License-Identifier: MPL-2.0
|
|
1
|
+
'''opencos.util -- support global logging, argparser for printing (colors)'''
|
|
3
2
|
|
|
4
3
|
import sys
|
|
5
4
|
import subprocess
|
|
@@ -10,18 +9,70 @@ import atexit
|
|
|
10
9
|
import shutil
|
|
11
10
|
import traceback
|
|
12
11
|
import argparse
|
|
13
|
-
import yaml
|
|
14
12
|
import re
|
|
15
|
-
import textwrap
|
|
16
13
|
|
|
17
|
-
from
|
|
14
|
+
from importlib import import_module
|
|
15
|
+
|
|
16
|
+
global_exit_allowed = False # pylint: disable=invalid-name
|
|
17
|
+
progname = "UNKNOWN" # pylint: disable=invalid-name
|
|
18
|
+
progname_in_message = True # pylint: disable=invalid-name
|
|
19
|
+
debug_level = 0 # pylint: disable=invalid-name
|
|
20
|
+
|
|
21
|
+
args = { # pylint: disable=invalid-name
|
|
22
|
+
'color' : False,
|
|
23
|
+
'quiet' : False,
|
|
24
|
+
'verbose' : False,
|
|
25
|
+
'debug' : False,
|
|
26
|
+
'fancy' : sys.stdout.isatty(),
|
|
27
|
+
'warnings' : 0,
|
|
28
|
+
'errors' : 0,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class Colors:
|
|
32
|
+
'''Namespace class for color printing help'''
|
|
33
|
+
red = "\x1B[31m"
|
|
34
|
+
green = "\x1B[32m"
|
|
35
|
+
orange = "\x1B[33m"
|
|
36
|
+
yellow = "\x1B[39m"
|
|
37
|
+
normal = "\x1B[0m"
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def color_text(text: str, color: str) -> str:
|
|
41
|
+
'''Wraps 'text' (str) with color (one of red|green|orange|yellow) prefix and
|
|
42
|
+
|
|
43
|
+
color (normal) suffix. Disables color prefix/suffix wrapping if args['color']=False
|
|
44
|
+
'''
|
|
45
|
+
if args['color']:
|
|
46
|
+
return color + text + "\x1B[0m" # (normal)
|
|
47
|
+
return text
|
|
48
|
+
|
|
49
|
+
def red_text(text: str) -> str:
|
|
50
|
+
'''Wraps text for printing as red, disabled if global args['color']=False'''
|
|
51
|
+
if args['color']:
|
|
52
|
+
return Colors.red + text + Colors.normal
|
|
53
|
+
return text
|
|
54
|
+
|
|
55
|
+
def green_text(text: str) -> str:
|
|
56
|
+
'''Wraps text for printing as green, disabled if global args['color']=False'''
|
|
57
|
+
if args['color']:
|
|
58
|
+
return Colors.green + text + Colors.normal
|
|
59
|
+
return text
|
|
60
|
+
|
|
61
|
+
def orange_text(text: str) -> str:
|
|
62
|
+
'''Wraps text for printing as orange, disabled if global args['color']=False'''
|
|
63
|
+
if args['color']:
|
|
64
|
+
return Colors.orange + text + Colors.normal
|
|
65
|
+
return text
|
|
66
|
+
|
|
67
|
+
def yellow_text(text: str) -> str:
|
|
68
|
+
'''Wraps text for printing as yellow, disabled if global args['color']=False'''
|
|
69
|
+
if args['color']:
|
|
70
|
+
return Colors.yellow + text + Colors.normal
|
|
71
|
+
return text
|
|
18
72
|
|
|
19
|
-
global_exit_allowed = False
|
|
20
|
-
progname = "UNKNOWN"
|
|
21
|
-
progname_in_message = True
|
|
22
|
-
debug_level = 0
|
|
23
73
|
|
|
24
74
|
class UtilLogger:
|
|
75
|
+
'''Class for the util.global_log'''
|
|
25
76
|
file = None
|
|
26
77
|
filepath = ''
|
|
27
78
|
time_last = 0 #timestamp via time.time()
|
|
@@ -32,11 +83,13 @@ class UtilLogger:
|
|
|
32
83
|
default_log_filepath = os.path.join('eda.work', 'eda.log')
|
|
33
84
|
|
|
34
85
|
def clear(self) -> None:
|
|
86
|
+
'''Resets internals'''
|
|
35
87
|
self.file = None
|
|
36
88
|
self.filepath = ''
|
|
37
89
|
self.time_last = 0
|
|
38
90
|
|
|
39
91
|
def stop(self) -> None:
|
|
92
|
+
'''Closes open log, resets internals'''
|
|
40
93
|
if self.file:
|
|
41
94
|
self.write_timestamp(f'stop - {self.filepath}')
|
|
42
95
|
info(f"Closing logfile: {self.filepath}")
|
|
@@ -44,8 +97,9 @@ class UtilLogger:
|
|
|
44
97
|
self.clear()
|
|
45
98
|
|
|
46
99
|
def start(self, filename: str, force: bool = False) -> None:
|
|
100
|
+
'''Starts (opens) log'''
|
|
47
101
|
if not filename:
|
|
48
|
-
error(
|
|
102
|
+
error('Trying to start a logfile, but filename is missing')
|
|
49
103
|
return
|
|
50
104
|
if os.path.exists(filename):
|
|
51
105
|
if force:
|
|
@@ -57,7 +111,7 @@ class UtilLogger:
|
|
|
57
111
|
else:
|
|
58
112
|
safe_mkdir_for_file(filename)
|
|
59
113
|
try:
|
|
60
|
-
self.file = open(filename, 'w')
|
|
114
|
+
self.file = open(filename, 'w', encoding='utf-8') # pylint: disable=consider-using-with
|
|
61
115
|
debug(f"Opened logfile '{filename}' for writing")
|
|
62
116
|
self.filepath = filename
|
|
63
117
|
self.write_timestamp(f'start - {self.filepath}')
|
|
@@ -66,17 +120,24 @@ class UtilLogger:
|
|
|
66
120
|
self.clear()
|
|
67
121
|
|
|
68
122
|
def write_timestamp(self, text: str = "") -> None:
|
|
123
|
+
'''Writes timestamp to opened log'''
|
|
124
|
+
if not self.file:
|
|
125
|
+
return
|
|
69
126
|
dt = datetime.datetime.now().ctime()
|
|
70
127
|
print(f"INFO: [{progname}] Time: {dt} {text}", file=self.file)
|
|
71
128
|
self.time_last = time.time()
|
|
72
129
|
|
|
73
|
-
def write(self, text: str, end: str) -> None:
|
|
74
|
-
|
|
75
|
-
if
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
130
|
+
def write(self, text: str, end: str = '\n') -> None:
|
|
131
|
+
'''Writes text to opened log'''
|
|
132
|
+
if not self.file:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if ((time.time() - self.time_last) > 10) and \
|
|
136
|
+
any(text.startswith(x) for x in [
|
|
137
|
+
f"DEBUG: [{progname}]",
|
|
138
|
+
f"INFO: [{progname}]",
|
|
139
|
+
f"WARNING: [{progname}]",
|
|
140
|
+
f"ERROR: [{progname}]"]):
|
|
80
141
|
self.write_timestamp()
|
|
81
142
|
print(text, end=end, file=self.file)
|
|
82
143
|
self.file.flush()
|
|
@@ -86,134 +147,28 @@ class UtilLogger:
|
|
|
86
147
|
global_log = UtilLogger()
|
|
87
148
|
|
|
88
149
|
|
|
89
|
-
def start_log(filename, force=False):
|
|
150
|
+
def start_log(filename: str, force: bool = False) -> None:
|
|
151
|
+
'''Starts the global_log, if not already started'''
|
|
90
152
|
global_log.start(filename=filename, force=force)
|
|
91
153
|
|
|
92
|
-
def write_log(text, end):
|
|
154
|
+
def write_log(text: str, end: str = '\n') -> None:
|
|
155
|
+
'''Writes to the global_log, if started'''
|
|
93
156
|
global_log.write(text=text, end=end)
|
|
94
157
|
|
|
95
|
-
def stop_log():
|
|
158
|
+
def stop_log() -> None:
|
|
159
|
+
'''Stops/closed the global_log'''
|
|
96
160
|
global_log.stop()
|
|
97
161
|
|
|
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
162
|
|
|
163
|
+
atexit.register(stop_log)
|
|
158
164
|
|
|
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
165
|
|
|
162
|
-
|
|
166
|
+
def get_argparse_bool_action_kwargs() -> dict:
|
|
167
|
+
'''Returns args for BooleanOptionalAction kwargs for an ArgumentParser.add_argument
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
all the root level keys. Used for debugging DEPS.yml in
|
|
166
|
-
eda.CommandDesign.resolve_target_core
|
|
169
|
+
This is mostly for python compatibility to support --some-enable and --no-some-enable
|
|
167
170
|
'''
|
|
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()
|
|
171
|
+
bool_kwargs = {}
|
|
217
172
|
x = getattr(argparse, 'BooleanOptionalAction', None)
|
|
218
173
|
if x is not None:
|
|
219
174
|
bool_kwargs['action'] = x
|
|
@@ -221,8 +176,12 @@ def get_argparse_bool_action_kwargs() -> dict:
|
|
|
221
176
|
bool_kwargs['action'] = 'store_true'
|
|
222
177
|
return bool_kwargs
|
|
223
178
|
|
|
179
|
+
|
|
224
180
|
def get_argparser() -> argparse.ArgumentParser:
|
|
225
|
-
|
|
181
|
+
'''Returns the opencos.util ArgumentParser'''
|
|
182
|
+
parser = argparse.ArgumentParser(
|
|
183
|
+
prog='opencos common options', add_help=False, allow_abbrev=False
|
|
184
|
+
)
|
|
226
185
|
# We set allow_abbrev=False so --force-logfile won't try to attempt parsing shorter similarly
|
|
227
186
|
# named args like --force, we want those to go to unparsed list.
|
|
228
187
|
# For bools, support --color and --no-color with this action=argparse.BooleanOptionalAction
|
|
@@ -256,7 +215,9 @@ def get_argparser() -> argparse.ArgumentParser:
|
|
|
256
215
|
' using $OC_ROOT/bin'))
|
|
257
216
|
return parser
|
|
258
217
|
|
|
259
|
-
|
|
218
|
+
|
|
219
|
+
def get_argparser_short_help(parser: object = None) -> str:
|
|
220
|
+
'''Returns short help for our ArgumentParser'''
|
|
260
221
|
if not parser:
|
|
261
222
|
parser = get_argparser()
|
|
262
223
|
full_lines = parser.format_help().split('\n')
|
|
@@ -268,18 +229,25 @@ def get_argparser_short_help(parser=None) -> str:
|
|
|
268
229
|
return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
|
|
269
230
|
|
|
270
231
|
|
|
271
|
-
def process_token(arg:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
232
|
+
def process_token(arg: list) -> bool:
|
|
233
|
+
'''Returns true if we parsed arg (list, because we may pass --arg value)
|
|
234
|
+
|
|
235
|
+
This is legacy holdover for oc_cli.py, that would process one token at a time.
|
|
236
|
+
Simply run through our full argparser.
|
|
237
|
+
'''
|
|
238
|
+
_, unparsed = process_tokens(arg)
|
|
239
|
+
if not unparsed: # empty list
|
|
240
|
+
debug(f"Processed token or pair: {arg}")
|
|
277
241
|
return True
|
|
278
242
|
return False
|
|
279
243
|
|
|
280
244
|
|
|
281
245
|
def process_tokens(tokens:list) -> (argparse.Namespace, list):
|
|
282
|
-
|
|
246
|
+
'''Processes tokens (unparsed args list) on util's ArgumentParser
|
|
247
|
+
|
|
248
|
+
Returns tuple of (parsed Namespace, unparsed args list)
|
|
249
|
+
'''
|
|
250
|
+
global debug_level # pylint: disable=global-statement
|
|
283
251
|
|
|
284
252
|
parser = get_argparser()
|
|
285
253
|
try:
|
|
@@ -288,9 +256,12 @@ def process_tokens(tokens:list) -> (argparse.Namespace, list):
|
|
|
288
256
|
except argparse.ArgumentError:
|
|
289
257
|
error(f'problem attempting to parse_known_args for {tokens=}')
|
|
290
258
|
|
|
291
|
-
if parsed.debug_level:
|
|
292
|
-
|
|
293
|
-
|
|
259
|
+
if parsed.debug_level:
|
|
260
|
+
set_debug_level(parsed.debug_level)
|
|
261
|
+
elif parsed.debug:
|
|
262
|
+
set_debug_level(1)
|
|
263
|
+
else:
|
|
264
|
+
debug_level = 0
|
|
294
265
|
|
|
295
266
|
debug(f'util.process_tokens: {parsed=} {unparsed=} from {tokens=}')
|
|
296
267
|
|
|
@@ -308,236 +279,293 @@ def process_tokens(tokens:list) -> (argparse.Namespace, list):
|
|
|
308
279
|
for key,value in parsed_as_dict.items():
|
|
309
280
|
if value is not None:
|
|
310
281
|
args[key] = value # only update with non-None values to our global 'args' dict
|
|
311
|
-
return parsed, unparsed
|
|
312
282
|
|
|
313
|
-
|
|
314
|
-
"""Returns str, wraps text to a specified width and indents subsequent lines."""
|
|
315
|
-
wrapped_lines = textwrap.wrap(text, width=width,
|
|
316
|
-
initial_indent=' ' * initial_indent,
|
|
317
|
-
subsequent_indent=' ' * indent)
|
|
318
|
-
return '\n'.join(wrapped_lines)
|
|
283
|
+
return parsed, unparsed
|
|
319
284
|
|
|
320
285
|
# ********************
|
|
321
286
|
# 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
|
-
|
|
287
|
+
# In fancy mode, we take the bottom _FANCY_LINES lines of the screen to be written using
|
|
288
|
+
# fancy_print, while the lines above that show regular scrolling content (via info, debug, warning,
|
|
289
|
+
# error above). User should not use print() when in fancy mode
|
|
290
|
+
|
|
291
|
+
_FANCY_LINES = []
|
|
292
|
+
|
|
293
|
+
def fancy_start(fancy_lines: int = 4, min_vanilla_lines: int = 4) -> None:
|
|
294
|
+
'''Starts fancy line support. This is not called by util internally
|
|
295
|
+
|
|
296
|
+
It is called by an opencos.eda Command handling class, it can check if util.args['fancy']
|
|
297
|
+
is set
|
|
298
|
+
'''
|
|
299
|
+
_, lines = shutil.get_terminal_size()
|
|
300
|
+
if fancy_lines < 2:
|
|
301
|
+
error("Fancy mode requires at least 2 fancy lines")
|
|
302
|
+
if fancy_lines > (lines - min_vanilla_lines):
|
|
303
|
+
error(f"Fancy mode supports at most {(lines - min_vanilla_lines)} fancy lines, given",
|
|
304
|
+
f"{min_vanilla_lines} non-fancy lines")
|
|
305
|
+
if _FANCY_LINES:
|
|
306
|
+
error("We are already in fancy line mode, cannot call fancy_start() again")
|
|
307
|
+
for _ in range(fancy_lines - 1):
|
|
308
|
+
print() # create the requisite number of blank lines
|
|
309
|
+
_FANCY_LINES.append("")
|
|
339
310
|
print("", end="") # the last line has no "\n" because we don't want ANOTHER blank line below
|
|
340
|
-
|
|
311
|
+
_FANCY_LINES.append("")
|
|
341
312
|
# the cursor remains at the leftmost character of the bottom line of the screen
|
|
342
313
|
|
|
343
|
-
def fancy_stop():
|
|
344
|
-
|
|
345
|
-
if
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
314
|
+
def fancy_stop() -> None:
|
|
315
|
+
'''Stops fancy mode. Intended to be called by an opencos.eda Command handling class'''
|
|
316
|
+
if not _FANCY_LINES:
|
|
317
|
+
# don't do anything if we aren't in fancy mode.
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
# user is expected to have painted something into the fancy lines, we can't "pull down"
|
|
321
|
+
# the regular lines above, and we don't want _FANCY_LINES blank or garbage lines either,
|
|
322
|
+
# that's not pretty
|
|
323
|
+
_FANCY_LINES.clear()
|
|
324
|
+
# since cursor is always left at the leftmost character of the bottom line of the screen,
|
|
325
|
+
# which was one of the fancy lines which now has the above-mentioned "something", we want
|
|
326
|
+
# to move one lower
|
|
327
|
+
print()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def fancy_print(text: str, line: int) -> None:
|
|
331
|
+
'''Fancy print, intended to be called by an opencos.eda Command handling class'''
|
|
332
|
+
|
|
355
333
|
# strip any newline, we don't want to print that
|
|
356
|
-
|
|
357
|
-
lines_above = len(
|
|
334
|
+
text = text.rstrip("\n")
|
|
335
|
+
lines_above = len(_FANCY_LINES) - line - 1
|
|
358
336
|
if lines_above:
|
|
359
|
-
print(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
337
|
+
print(
|
|
338
|
+
(f"\033[{lines_above}A" # move cursor up
|
|
339
|
+
f"{text}\033[1G" # desired text, then move cursor to the first character of the line
|
|
340
|
+
f"\033[{lines_above}B" # move the cursor down
|
|
341
|
+
),
|
|
342
|
+
end="", flush=True
|
|
343
|
+
)
|
|
363
344
|
else:
|
|
364
|
-
print(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
345
|
+
print(
|
|
346
|
+
f"{text}\033[1G", # desired text, then move cursor to the first character of the line
|
|
347
|
+
end="", flush=True
|
|
348
|
+
)
|
|
349
|
+
_FANCY_LINES[line] = text
|
|
350
|
+
|
|
351
|
+
def print_pre() -> None:
|
|
352
|
+
'''called by all util info/warning/debug/error. Handles fancy mode'''
|
|
369
353
|
# stuff we do before printing any line
|
|
370
|
-
if
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
354
|
+
if not _FANCY_LINES:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
# Also, note that in fancy mode we don't allow the "above lines" to be partially written, they
|
|
358
|
+
# are assumed to be full lines ending in "\n"
|
|
359
|
+
# As always, we expect the cursor was left in the leftmost character of bottom line of screen
|
|
360
|
+
print(
|
|
361
|
+
(f"\033[{len(_FANCY_LINES)-1}A" # move the cursor up to where the first fancy line is drawn
|
|
362
|
+
f"\033[0K" # clear the old fancy line 0
|
|
363
|
+
),
|
|
364
|
+
end="", flush=True
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def print_post(text: str, end: str) -> None:
|
|
368
|
+
'''called by all util info/warning/debug/error. Handles fancy mode'''
|
|
379
369
|
# stuff we do after printing any line
|
|
380
|
-
if
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
print(
|
|
387
|
-
|
|
388
|
-
|
|
370
|
+
if _FANCY_LINES:
|
|
371
|
+
# we just printed a line, including a new line, on top of where fancy line 0 used to be,
|
|
372
|
+
# so cursor is now at the start of fancy line 1. move cursor down to the beginning of the
|
|
373
|
+
# final fancy line (i.e. standard fancy cursor resting place)
|
|
374
|
+
for x, line in enumerate(_FANCY_LINES):
|
|
375
|
+
print("\033[0K", end="") # erase the line to the right
|
|
376
|
+
print(line, flush=True,
|
|
377
|
+
end=('' if x == (len(_FANCY_LINES) - 1) else '\n'))
|
|
378
|
+
|
|
389
379
|
print("\033[1G", end="", flush=True)
|
|
390
|
-
if global_log.file:
|
|
380
|
+
if global_log.file:
|
|
381
|
+
write_log(text, end=end)
|
|
391
382
|
|
|
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
383
|
|
|
398
|
-
def
|
|
384
|
+
def print_color(text: str, color: str, end: str = '\n') -> None:
|
|
385
|
+
'''Note that color(str) must be one of Colors.[red|green|orange|yellow|normal]'''
|
|
399
386
|
print_pre()
|
|
400
|
-
print(
|
|
387
|
+
print(Colors.color_text(text, color), end=end, flush=True)
|
|
401
388
|
print_post(text, end)
|
|
402
389
|
|
|
403
|
-
def
|
|
390
|
+
def print_red(text: str, end: str = '\n') -> None:
|
|
391
|
+
'''Print text as red, goes back to normal color'''
|
|
404
392
|
print_pre()
|
|
405
|
-
print(
|
|
393
|
+
print(red_text(text), end=end, flush=True)
|
|
406
394
|
print_post(text, end)
|
|
407
395
|
|
|
408
|
-
def
|
|
396
|
+
def print_green(text: str, end: str = '\n') -> None:
|
|
397
|
+
'''Print text as green, goes back to normal color'''
|
|
409
398
|
print_pre()
|
|
410
|
-
print(
|
|
399
|
+
print(green_text(text), end=end, flush=True)
|
|
411
400
|
print_post(text, end)
|
|
412
401
|
|
|
413
|
-
def
|
|
402
|
+
def print_orange(text: str, end: str = '\n') -> None:
|
|
403
|
+
'''Print text as orange, goes back to normal color'''
|
|
414
404
|
print_pre()
|
|
415
|
-
print(
|
|
405
|
+
print(orange_text(text), end=end, flush=True)
|
|
416
406
|
print_post(text, end)
|
|
417
407
|
|
|
418
|
-
def
|
|
419
|
-
|
|
408
|
+
def print_yellow(text: str, end: str = '\n') -> None:
|
|
409
|
+
'''Print text as yellow, goes back to normal color'''
|
|
410
|
+
print_pre()
|
|
411
|
+
print(yellow_text(text), end=end, flush=True)
|
|
412
|
+
print_post(text, end)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def set_debug_level(level) -> None:
|
|
416
|
+
'''Sets global debug level, sets args['debug'] and args['verbose']'''
|
|
417
|
+
global debug_level # pylint: disable=global-statement
|
|
420
418
|
debug_level = level
|
|
421
|
-
args['debug'] =
|
|
422
|
-
args['verbose'] =
|
|
419
|
+
args['debug'] = level > 0
|
|
420
|
+
args['verbose'] = level > 1
|
|
423
421
|
info(f"Set debug level to {debug_level}")
|
|
424
422
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
423
|
+
|
|
424
|
+
def debug(*text, level: int = 1, start: object = None, end: str = '\n') -> None:
|
|
425
|
+
'''Print debug messaging (in yellow if possible). If args['debug'] is false, prints nothing.
|
|
426
|
+
|
|
427
|
+
*text: (positional str args) to be printed
|
|
428
|
+
level: (int) debug level to decide if printed or not.
|
|
429
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
430
|
+
end: (optional str) suffix to print
|
|
431
|
+
|
|
432
|
+
Note these messages append to global logging (but require args['debug'] to be set)
|
|
433
|
+
'''
|
|
434
|
+
if start is None:
|
|
435
|
+
start = "DEBUG: " + (f"[{progname}] " if progname_in_message else "")
|
|
436
|
+
if args['debug'] and \
|
|
437
|
+
(((level==1) and args['verbose']) or (debug_level >= level)):
|
|
430
438
|
print_yellow(f"{start}{' '.join(list(text))}", end=end)
|
|
431
439
|
|
|
432
|
-
|
|
433
|
-
|
|
440
|
+
|
|
441
|
+
def info(*text, start: object = None, end='\n') -> None:
|
|
442
|
+
'''Print information messaging (in green if possible). If args['quiet'], prints nothing.
|
|
443
|
+
|
|
444
|
+
*text: (positional str args) to be printed
|
|
445
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
446
|
+
end: (optional str) suffix to print
|
|
447
|
+
|
|
448
|
+
Note these messages append to global logging even if args['quiet'] is set
|
|
449
|
+
'''
|
|
450
|
+
if start is None:
|
|
451
|
+
start = "INFO: " + (f"[{progname}] " if progname_in_message else "")
|
|
434
452
|
if not args['quiet']:
|
|
435
453
|
print_green(f"{start}{' '.join(list(text))}", end=end)
|
|
436
454
|
|
|
437
|
-
def warning(*text, start=
|
|
438
|
-
|
|
455
|
+
def warning(*text, start: object = None, end: str = '\n') -> None:
|
|
456
|
+
'''Print warning messaging (in orange if possible).
|
|
457
|
+
|
|
458
|
+
*text: (positional str args) to be printed
|
|
459
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
460
|
+
end: (optional str) suffix to print
|
|
461
|
+
|
|
462
|
+
Note these messages append to global logging. Increments global args['warnings'] int.
|
|
463
|
+
'''
|
|
464
|
+
if start is None:
|
|
465
|
+
start = "WARNING: " + (f"[{progname}] " if progname_in_message else "")
|
|
439
466
|
args['warnings'] += 1
|
|
440
467
|
print_orange(f"{start}{' '.join(list(text))}", end=end)
|
|
441
468
|
|
|
442
|
-
|
|
443
|
-
|
|
469
|
+
|
|
470
|
+
def error(
|
|
471
|
+
*text, error_code: int = 255, do_exit: bool = True, start: object = None, end: str = '\n'
|
|
472
|
+
) -> int:
|
|
473
|
+
'''Print error messaging (in red if possible).
|
|
474
|
+
|
|
475
|
+
*text: (positional str args) to be printed
|
|
476
|
+
error_code: (int) shell style return code (non-zero is error, but prefer > 1 b/c those are
|
|
477
|
+
python exceptions)
|
|
478
|
+
do_exit: (bool) if True will call exit based on global_exit_allowed.
|
|
479
|
+
start: (optional str) prefix to message; if None: chooses default start str
|
|
480
|
+
end: (optional str) suffix to print
|
|
481
|
+
|
|
482
|
+
Note these messages append to global logging. Increments global args['errors'] int.
|
|
483
|
+
'''
|
|
484
|
+
if start is None:
|
|
485
|
+
start = "ERROR: " + (f"[{progname}] " if progname_in_message else "")
|
|
444
486
|
args['errors'] += 1
|
|
445
487
|
print_red(f"{start}{' '.join(list(text))}", end=end)
|
|
446
488
|
if do_exit:
|
|
447
|
-
if args['debug']:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
489
|
+
if args['debug']:
|
|
490
|
+
print(traceback.print_stack())
|
|
491
|
+
# Call our overriden-builtin function for 'exit':
|
|
492
|
+
return exit(error_code) # pylint: disable=consider-using-sys-exit
|
|
493
|
+
|
|
494
|
+
if error_code is None:
|
|
495
|
+
return 0
|
|
496
|
+
return abs(int(error_code))
|
|
497
|
+
|
|
454
498
|
|
|
455
|
-
def exit(
|
|
499
|
+
def exit( # pylint: disable=redefined-builtin
|
|
500
|
+
error_code: int = 0, quiet: bool = False
|
|
501
|
+
) -> int:
|
|
502
|
+
'''sys.exit(int) wrapper, returns the error_code if global_exit_allowed=False'''
|
|
456
503
|
if global_exit_allowed:
|
|
457
|
-
if not quiet:
|
|
504
|
+
if not quiet:
|
|
505
|
+
info(f"Exiting with {args['warnings']} warnings, {args['errors']} errors")
|
|
458
506
|
sys.exit(error_code)
|
|
459
507
|
|
|
460
508
|
if error_code is None:
|
|
461
509
|
return 0
|
|
462
|
-
|
|
463
|
-
|
|
510
|
+
|
|
511
|
+
return abs(int(error_code))
|
|
512
|
+
|
|
464
513
|
|
|
465
514
|
def getcwd():
|
|
515
|
+
'''Wrapper for os.getcwd() for current working dir'''
|
|
466
516
|
try:
|
|
467
|
-
|
|
517
|
+
return os.getcwd()
|
|
468
518
|
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
|
|
519
|
+
error(f"Unable to getcwd(), did it get deleted from under us? Exception: {e}")
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
_OC_ROOT = None
|
|
523
|
+
_OC_ROOT_SET = False
|
|
524
|
+
|
|
525
|
+
def get_oc_root(error_on_fail: bool = False) -> None:
|
|
477
526
|
'''Returns a str or False for the root directory of *this* repo.
|
|
478
527
|
|
|
479
528
|
If environment variable OC_ROOT is set, that is used instead, otherwise attempts to use
|
|
480
529
|
`git rev-parse`
|
|
481
530
|
'''
|
|
531
|
+
|
|
532
|
+
global _OC_ROOT # pylint: disable=global-statement
|
|
533
|
+
global _OC_ROOT_SET # pylint: disable=global-statement
|
|
482
534
|
# if we've already run through here once, just return the memorized result
|
|
483
|
-
if
|
|
535
|
+
if _OC_ROOT_SET:
|
|
536
|
+
return _OC_ROOT
|
|
484
537
|
|
|
485
538
|
# try looking for an env var
|
|
486
539
|
s = os.environ.get('OC_ROOT')
|
|
487
540
|
if s:
|
|
488
541
|
debug(f'get_oc_root() -- returning from env: {s=}')
|
|
489
|
-
|
|
542
|
+
_OC_ROOT = s.strip()
|
|
490
543
|
else:
|
|
491
544
|
# try asking GIT
|
|
492
|
-
cp = subprocess.run(
|
|
493
|
-
|
|
545
|
+
cp = subprocess.run(
|
|
546
|
+
'git rev-parse --show-toplevel', stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
547
|
+
shell=True, check=False, universal_newlines=True
|
|
548
|
+
)
|
|
494
549
|
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
|
|
550
|
+
# TODO(drew): at some point, address the fact that not all repos are oc_root.
|
|
551
|
+
# Is this function asking for the repo we are in? or a pointer to the oc_root which
|
|
552
|
+
# maybe elsewhere on the system?
|
|
497
553
|
print_didnt_find_it = debug
|
|
498
554
|
if error_on_fail:
|
|
499
555
|
print_didnt_find_it = error
|
|
500
|
-
print_didnt_find_it(
|
|
556
|
+
print_didnt_find_it('Unable to get a OC_ROOT directory using git rev-parse')
|
|
501
557
|
else:
|
|
502
|
-
|
|
503
|
-
if sys.platform
|
|
504
|
-
|
|
558
|
+
_OC_ROOT = cp.stdout.strip()
|
|
559
|
+
if sys.platform.startswith('win'):
|
|
560
|
+
_OC_ROOT = _OC_ROOT.replace('/', '\\') # git gives us /, but we need \
|
|
505
561
|
|
|
506
562
|
# there is no sense running through this code more than once
|
|
507
|
-
|
|
508
|
-
return
|
|
563
|
+
_OC_ROOT_SET = True
|
|
564
|
+
return _OC_ROOT
|
|
509
565
|
|
|
510
|
-
def string_or_space(text, whitespace=False):
|
|
511
|
-
if whitespace:
|
|
512
|
-
return " " * len(text)
|
|
513
|
-
else:
|
|
514
|
-
return text
|
|
515
566
|
|
|
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):
|
|
567
|
+
def safe_cp(source: str, destination: str, create_dirs: bool = False) -> None:
|
|
568
|
+
'''shutil.copy2 wrapper to optionally make the destination directories'''
|
|
541
569
|
try:
|
|
542
570
|
# Infer if destination is a directory
|
|
543
571
|
if destination.endswith('/') or os.path.isdir(destination):
|
|
@@ -551,51 +579,45 @@ def safe_cp(source:str, destination:str, create_dirs:bool=False):
|
|
|
551
579
|
os.makedirs(parent_dir, exist_ok=True)
|
|
552
580
|
# actually copy the file
|
|
553
581
|
shutil.copy2(source, destination)
|
|
582
|
+
info(f"Copied {source} to {destination}")
|
|
554
583
|
except Exception as e:
|
|
555
584
|
print(f"Error copying file from '{source}' to '{destination}': {e}")
|
|
556
|
-
info(f"Copied {source} to {destination}")
|
|
557
585
|
|
|
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
586
|
|
|
570
|
-
def safe_mkdir(path : str):
|
|
587
|
+
def safe_mkdir(path : str) -> None:
|
|
588
|
+
'''Attempt to make dir at path, and make all subdirs up to that path'''
|
|
571
589
|
if os.path.exists(path):
|
|
572
590
|
return
|
|
573
|
-
left,
|
|
591
|
+
left, _ = os.path.split(os.path.relpath(path))
|
|
574
592
|
if left and left not in ['.', '..', os.path.sep]:
|
|
575
593
|
safe_mkdir(left)
|
|
576
594
|
try:
|
|
577
595
|
os.mkdir(path)
|
|
578
596
|
except FileExistsError:
|
|
579
597
|
pass
|
|
580
|
-
except:
|
|
598
|
+
except Exception as e1:
|
|
581
599
|
try:
|
|
582
600
|
os.system(f'mkdir -p {path}')
|
|
583
|
-
except Exception as
|
|
584
|
-
error(f'unable to mkdir {path=},
|
|
601
|
+
except Exception as e2:
|
|
602
|
+
error(f'unable to mkdir {path=}, exceptions {e1}, {e2=}')
|
|
585
603
|
|
|
586
|
-
|
|
604
|
+
|
|
605
|
+
def safe_mkdirs(base : str, new_dirs : list) -> None:
|
|
606
|
+
'''Create new_dirs at base'''
|
|
587
607
|
for p in new_dirs:
|
|
588
608
|
safe_mkdir( os.path.join(base, p) )
|
|
589
609
|
|
|
590
|
-
|
|
591
|
-
|
|
610
|
+
|
|
611
|
+
def safe_mkdir_for_file(filepath: str) -> None:
|
|
612
|
+
'''Given a new filepath, create dir for that filepath'''
|
|
613
|
+
left, _ = os.path.split(filepath)
|
|
592
614
|
if left:
|
|
593
615
|
safe_mkdir(left)
|
|
594
616
|
|
|
595
617
|
|
|
596
|
-
def import_class_from_string(full_class_name):
|
|
618
|
+
def import_class_from_string(full_class_name: str) -> None:
|
|
597
619
|
"""
|
|
598
|
-
Imports a class given its full name as a
|
|
620
|
+
Imports a class given its full name as a str.
|
|
599
621
|
|
|
600
622
|
Args:
|
|
601
623
|
full_class_name: The full name of the class,
|
|
@@ -604,18 +626,21 @@ def import_class_from_string(full_class_name):
|
|
|
604
626
|
Returns:
|
|
605
627
|
The imported class, or None if an error occurs.
|
|
606
628
|
"""
|
|
607
|
-
from importlib import import_module
|
|
608
629
|
try:
|
|
609
630
|
module_path, class_name = full_class_name.rsplit(".", 1)
|
|
610
631
|
module = import_module(module_path)
|
|
611
632
|
return getattr(module, class_name)
|
|
612
|
-
except
|
|
613
|
-
print(f"Error importing class {full_class_name=}: {e
|
|
633
|
+
except Exception as e:
|
|
634
|
+
print(f"Error importing class {full_class_name=}: {e}")
|
|
614
635
|
return None
|
|
615
636
|
|
|
616
637
|
|
|
617
638
|
class ShellCommandList(list):
|
|
618
|
-
|
|
639
|
+
'''Wrapper around a list, of str that we'll run as a subprocess command
|
|
640
|
+
|
|
641
|
+
included member var for tee_path, to save a log from this subprocess commands list
|
|
642
|
+
'''
|
|
643
|
+
def __init__(self, obj: object = None, tee_fpath: str = ''):
|
|
619
644
|
super().__init__(obj)
|
|
620
645
|
for k in ['tee_fpath']:
|
|
621
646
|
setattr(self, k, getattr(obj, k, None))
|
|
@@ -623,29 +648,35 @@ class ShellCommandList(list):
|
|
|
623
648
|
self.tee_fpath = tee_fpath
|
|
624
649
|
|
|
625
650
|
|
|
626
|
-
def write_shell_command_file(
|
|
627
|
-
|
|
651
|
+
def write_shell_command_file(
|
|
652
|
+
dirpath : str, filename : str, command_lists : list, line_breaks : bool = False
|
|
653
|
+
) -> None:
|
|
654
|
+
''' Writes new file at {dirpath}/{filename} as a bash shell command, using command_lists
|
|
655
|
+
(list of lists)
|
|
628
656
|
|
|
629
657
|
-- dirpath (str) -- directory where file is written (usually eda.work/{target}_sim
|
|
630
658
|
-- 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
|
-
|
|
659
|
+
-- command_lists (list) -- list of (list or ShellCommandList), each item in the list is a
|
|
660
|
+
list of commands (aka, how subprocess.run(args) uses a list of
|
|
661
|
+
commands.
|
|
662
|
+
-- line_breaks (bool) -- Set to True to have 1 word per line in the file followed by a line
|
|
663
|
+
break. Default False has an entry in command_lists all on a single
|
|
664
|
+
line.
|
|
635
665
|
|
|
636
666
|
Returns None, writes the file and chmod's it to 0x755.
|
|
637
|
-
|
|
638
667
|
'''
|
|
668
|
+
|
|
639
669
|
# command_lists should be a list-of-lists.
|
|
640
670
|
bash_path = shutil.which('bash')
|
|
641
|
-
assert
|
|
671
|
+
assert isinstance(command_lists, list), f'{command_lists=}'
|
|
642
672
|
fullpath = os.path.join(dirpath, filename)
|
|
643
|
-
with open(fullpath, 'w') as f:
|
|
673
|
+
with open(fullpath, 'w', encoding='utf-8') as f:
|
|
644
674
|
if not bash_path:
|
|
645
675
|
bash_path = "/bin/bash" # we may not get far, but we'll try
|
|
646
676
|
f.write('#!' + bash_path + '\n\n')
|
|
647
677
|
for obj in command_lists:
|
|
648
|
-
assert isinstance(obj, list),
|
|
678
|
+
assert isinstance(obj, list), \
|
|
679
|
+
f'{obj=} (obj must be list/ShellCommandList) {command_lists=}'
|
|
649
680
|
clist = list(obj).copy()
|
|
650
681
|
tee_fpath = getattr(obj, 'tee_fpath', None)
|
|
651
682
|
if tee_fpath:
|
|
@@ -657,7 +688,7 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
|
|
|
657
688
|
else:
|
|
658
689
|
clist.append(f'2>&1 | tee {tee_fpath}')
|
|
659
690
|
|
|
660
|
-
if
|
|
691
|
+
if clist:
|
|
661
692
|
if line_breaks:
|
|
662
693
|
# line_breaks=True - have 1 word per line, followed by \ and \n
|
|
663
694
|
sep = " \\" + "\n"
|
|
@@ -674,26 +705,6 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
|
|
|
674
705
|
os.chmod(fullpath, 0o755)
|
|
675
706
|
|
|
676
707
|
|
|
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
708
|
def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
|
|
698
709
|
'''Returns the best guess as the 'top' module name name, given a fpath where
|
|
699
710
|
|
|
@@ -718,114 +729,9 @@ def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
|
|
|
718
729
|
if bool(re.fullmatch(r'^\w+$', module_name)):
|
|
719
730
|
if module_name == module_guess:
|
|
720
731
|
return module_guess
|
|
721
|
-
|
|
732
|
+
if module_name:
|
|
722
733
|
best_guess = module_name
|
|
723
734
|
if best_guess:
|
|
724
735
|
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
|
-
|
|
735
|
-
is_windows = sys.platform.startswith('win')
|
|
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
736
|
|
|
747
|
-
|
|
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
|
|
737
|
+
return ''
|