opencos-eda 0.2.47__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 +35 -18
- opencos/commands/sweep.py +9 -4
- opencos/commands/waves.py +1 -1
- 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 +92 -67
- opencos/eda_base.py +625 -332
- 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 +27 -14
- opencos/tests/test_deps_helpers.py +35 -32
- opencos/tests/test_eda.py +47 -41
- 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 +440 -483
- 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.47.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.47.dist-info → opencos_eda-0.2.49.dist-info}/entry_points.txt +1 -1
- opencos/deps_helpers.py +0 -1346
- opencos_eda-0.2.47.dist-info/RECORD +0 -79
- /opencos/{pcie.py → hw/pcie.py} +0 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/WHEEL +0 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE +0 -0
- {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE.spdx +0 -0
- {opencos_eda-0.2.47.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,22 +9,16 @@ 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
|
-
|
|
17
|
-
from opencos import eda_config
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
progname = "UNKNOWN"
|
|
21
|
-
progname_in_message = True
|
|
22
|
-
logfile = None
|
|
23
|
-
loglast = 0
|
|
24
|
-
debug_level = 0
|
|
14
|
+
from importlib import import_module
|
|
25
15
|
|
|
26
|
-
|
|
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
|
|
27
20
|
|
|
28
|
-
args = {
|
|
21
|
+
args = { # pylint: disable=invalid-name
|
|
29
22
|
'color' : False,
|
|
30
23
|
'quiet' : False,
|
|
31
24
|
'verbose' : False,
|
|
@@ -35,149 +28,147 @@ args = {
|
|
|
35
28
|
'errors' : 0,
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
try:
|
|
56
|
-
# Try to do a very lazy parse of root level keys only, returns dict{key:lineno}
|
|
57
|
-
data = dict()
|
|
58
|
-
for lineno,line in enumerate(f.readlines()):
|
|
59
|
-
m = re.match(r'^(\w+):', line)
|
|
60
|
-
if m:
|
|
61
|
-
key = m.group(1)
|
|
62
|
-
data[key] = lineno + 1
|
|
63
|
-
except Exception as e:
|
|
64
|
-
error(f"Error loading YAML {filepath=}:", e)
|
|
65
|
-
return data
|
|
66
|
-
|
|
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
|
|
67
48
|
|
|
68
|
-
def
|
|
69
|
-
'''
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class UtilLogger:
|
|
75
|
+
'''Class for the util.global_log'''
|
|
76
|
+
file = None
|
|
77
|
+
filepath = ''
|
|
78
|
+
time_last = 0 #timestamp via time.time()
|
|
79
|
+
|
|
80
|
+
# disabled by default, eda.py enables it. Can also be disabled via
|
|
81
|
+
# util's argparser: --no-default-log, --logfile=<name>, or --force-logfile=<name>
|
|
82
|
+
default_log_enabled = False
|
|
83
|
+
default_log_filepath = os.path.join('eda.work', 'eda.log')
|
|
84
|
+
|
|
85
|
+
def clear(self) -> None:
|
|
86
|
+
'''Resets internals'''
|
|
87
|
+
self.file = None
|
|
88
|
+
self.filepath = ''
|
|
89
|
+
self.time_last = 0
|
|
90
|
+
|
|
91
|
+
def stop(self) -> None:
|
|
92
|
+
'''Closes open log, resets internals'''
|
|
93
|
+
if self.file:
|
|
94
|
+
self.write_timestamp(f'stop - {self.filepath}')
|
|
95
|
+
info(f"Closing logfile: {self.filepath}")
|
|
96
|
+
self.file.close()
|
|
97
|
+
self.clear()
|
|
98
|
+
|
|
99
|
+
def start(self, filename: str, force: bool = False) -> None:
|
|
100
|
+
'''Starts (opens) log'''
|
|
101
|
+
if not filename:
|
|
102
|
+
error('Trying to start a logfile, but filename is missing')
|
|
103
|
+
return
|
|
104
|
+
if os.path.exists(filename):
|
|
105
|
+
if force:
|
|
106
|
+
debug(f"Overwriting logfile '{filename}', which exists, due to --force-logfile.")
|
|
107
|
+
else:
|
|
108
|
+
error(f"The --logfile path '{filename}' exists. Use --force-logfile",
|
|
109
|
+
"(vs --logfile) to override.")
|
|
110
|
+
return
|
|
111
|
+
else:
|
|
112
|
+
safe_mkdir_for_file(filename)
|
|
72
113
|
try:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
key = m.group(1)
|
|
78
|
-
data[key] = lineno + 1
|
|
114
|
+
self.file = open(filename, 'w', encoding='utf-8') # pylint: disable=consider-using-with
|
|
115
|
+
debug(f"Opened logfile '{filename}' for writing")
|
|
116
|
+
self.filepath = filename
|
|
117
|
+
self.write_timestamp(f'start - {self.filepath}')
|
|
79
118
|
except Exception as e:
|
|
80
|
-
error(f
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def yaml_safe_load(filepath:str, only_root_line_numbers=False):
|
|
85
|
-
'''Returns dict or None from filepath (str), errors if return type not in assert_return_types.
|
|
119
|
+
error(f"Error opening '{filename}' for writing, {e}")
|
|
120
|
+
self.clear()
|
|
86
121
|
|
|
87
|
-
(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
data = None
|
|
122
|
+
def write_timestamp(self, text: str = "") -> None:
|
|
123
|
+
'''Writes timestamp to opened log'''
|
|
124
|
+
if not self.file:
|
|
125
|
+
return
|
|
126
|
+
dt = datetime.datetime.now().ctime()
|
|
127
|
+
print(f"INFO: [{progname}] Time: {dt} {text}", file=self.file)
|
|
128
|
+
self.time_last = time.time()
|
|
95
129
|
|
|
96
|
-
|
|
97
|
-
|
|
130
|
+
def write(self, text: str, end: str = '\n') -> None:
|
|
131
|
+
'''Writes text to opened log'''
|
|
132
|
+
if not self.file:
|
|
133
|
+
return
|
|
98
134
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
sp_out = subprocess.run(
|
|
110
|
-
f'yamllint -d relaxed --no-warnings {filepath}'.split(),
|
|
111
|
-
capture_output=True, text=True )
|
|
112
|
-
for x in sp_out.stdout.split('\n'):
|
|
113
|
-
if x:
|
|
114
|
-
info('yamllint: ' + x)
|
|
115
|
-
except:
|
|
116
|
-
pass
|
|
117
|
-
|
|
118
|
-
if hasattr(e, 'problem_mark'):
|
|
119
|
-
mark = e.problem_mark
|
|
120
|
-
error(f"Error parsing {filepath=}: line {mark.line + 1},",
|
|
121
|
-
f"column {mark.column +1}: {e.problem}")
|
|
122
|
-
else:
|
|
123
|
-
error(f"Error loading YAML {filepath=}:", e)
|
|
124
|
-
except Exception as e:
|
|
125
|
-
error(f"Error loading YAML {filepath=}:", e)
|
|
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}]"]):
|
|
141
|
+
self.write_timestamp()
|
|
142
|
+
print(text, end=end, file=self.file)
|
|
143
|
+
self.file.flush()
|
|
144
|
+
os.fsync(self.file)
|
|
126
145
|
|
|
127
|
-
return data
|
|
128
146
|
|
|
147
|
+
global_log = UtilLogger()
|
|
129
148
|
|
|
130
|
-
def yaml_safe_writer(data:dict, filepath:str) -> None:
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
default_flow_style=False, sort_keys=False, encoding=('utf-8'))
|
|
136
|
-
else:
|
|
137
|
-
warning(f'{filepath=} to be written for this extension not implemented.')
|
|
150
|
+
def start_log(filename: str, force: bool = False) -> None:
|
|
151
|
+
'''Starts the global_log, if not already started'''
|
|
152
|
+
global_log.start(filename=filename, force=force)
|
|
138
153
|
|
|
154
|
+
def write_log(text: str, end: str = '\n') -> None:
|
|
155
|
+
'''Writes to the global_log, if started'''
|
|
156
|
+
global_log.write(text=text, end=end)
|
|
139
157
|
|
|
158
|
+
def stop_log() -> None:
|
|
159
|
+
'''Stops/closed the global_log'''
|
|
160
|
+
global_log.stop()
|
|
140
161
|
|
|
141
|
-
def start_log(filename, force=False):
|
|
142
|
-
global logfile, loglast
|
|
143
|
-
if os.path.exists(filename):
|
|
144
|
-
if force:
|
|
145
|
-
info(f"Overwriting '{filename}', which exists, due to --force-logfile.")
|
|
146
|
-
else:
|
|
147
|
-
error(f"The --logfile path '{filename}' exists. Use --force-logfile (vs --logfile) to override.")
|
|
148
|
-
try:
|
|
149
|
-
logfile = open( filename, 'w')
|
|
150
|
-
debug(f"Opened logfile '{filename}' for writing")
|
|
151
|
-
except Exception as e:
|
|
152
|
-
error(f"Error opening '{filename}' for writing!")
|
|
153
|
-
|
|
154
|
-
def write_log(text, end):
|
|
155
|
-
global logfile, loglast
|
|
156
|
-
sw = text.startswith(f"INFO: [{progname}]")
|
|
157
|
-
if (((time.time() - loglast) > 10) and
|
|
158
|
-
(text.startswith(f"DEBUG: [{progname}]") or
|
|
159
|
-
text.startswith(f"INFO: [{progname}]") or
|
|
160
|
-
text.startswith(f"WARNING: [{progname}]") or
|
|
161
|
-
text.startswith(f"ERROR: [{progname}]"))):
|
|
162
|
-
dt = datetime.datetime.now().ctime()
|
|
163
|
-
print(f"INFO: [{progname}] Time: {dt}", file=logfile)
|
|
164
|
-
loglast = time.time()
|
|
165
|
-
print(text, end=end, file=logfile)
|
|
166
|
-
logfile.flush()
|
|
167
|
-
os.fsync(logfile)
|
|
168
|
-
|
|
169
|
-
def stop_log():
|
|
170
|
-
global logfile, loglast
|
|
171
|
-
if logfile:
|
|
172
|
-
debug(f"Closing logfile")
|
|
173
|
-
logfile.close()
|
|
174
|
-
logfile = None
|
|
175
|
-
loglast = 0
|
|
176
162
|
|
|
177
163
|
atexit.register(stop_log)
|
|
178
164
|
|
|
165
|
+
|
|
179
166
|
def get_argparse_bool_action_kwargs() -> dict:
|
|
180
|
-
|
|
167
|
+
'''Returns args for BooleanOptionalAction kwargs for an ArgumentParser.add_argument
|
|
168
|
+
|
|
169
|
+
This is mostly for python compatibility to support --some-enable and --no-some-enable
|
|
170
|
+
'''
|
|
171
|
+
bool_kwargs = {}
|
|
181
172
|
x = getattr(argparse, 'BooleanOptionalAction', None)
|
|
182
173
|
if x is not None:
|
|
183
174
|
bool_kwargs['action'] = x
|
|
@@ -185,8 +176,12 @@ def get_argparse_bool_action_kwargs() -> dict:
|
|
|
185
176
|
bool_kwargs['action'] = 'store_true'
|
|
186
177
|
return bool_kwargs
|
|
187
178
|
|
|
179
|
+
|
|
188
180
|
def get_argparser() -> argparse.ArgumentParser:
|
|
189
|
-
|
|
181
|
+
'''Returns the opencos.util ArgumentParser'''
|
|
182
|
+
parser = argparse.ArgumentParser(
|
|
183
|
+
prog='opencos common options', add_help=False, allow_abbrev=False
|
|
184
|
+
)
|
|
190
185
|
# We set allow_abbrev=False so --force-logfile won't try to attempt parsing shorter similarly
|
|
191
186
|
# named args like --force, we want those to go to unparsed list.
|
|
192
187
|
# For bools, support --color and --no-color with this action=argparse.BooleanOptionalAction
|
|
@@ -205,15 +200,24 @@ def get_argparser() -> argparse.ArgumentParser:
|
|
|
205
200
|
help='Display additional debug messaging level 1 or higher')
|
|
206
201
|
parser.add_argument('--debug-level', type=int, default=0,
|
|
207
202
|
help='Set debug level messaging (default: 0)')
|
|
208
|
-
parser.add_argument('--logfile', type=str, default=
|
|
209
|
-
help='Write eda messaging to logfile
|
|
210
|
-
|
|
203
|
+
parser.add_argument('--logfile', type=str, default=None,
|
|
204
|
+
help=('Write eda messaging to safe logfile that will not be overwritten'
|
|
205
|
+
' (default disabled)'))
|
|
206
|
+
parser.add_argument('--force-logfile', type=str, default=None,
|
|
211
207
|
help='Set to force overwrite the logfile')
|
|
208
|
+
parser.add_argument('--default-log', **bool_action_kwargs,
|
|
209
|
+
default=global_log.default_log_enabled,
|
|
210
|
+
help=('Enable/Disable default logging to'
|
|
211
|
+
f' {global_log.default_log_filepath}. Default logging is disabled'
|
|
212
|
+
' if --logfile or --force-logfile is set'))
|
|
212
213
|
parser.add_argument('--no-respawn', action='store_true',
|
|
213
|
-
help='Legacy mode (default respawn disabled) for respawning eda.py
|
|
214
|
+
help=('Legacy mode (default respawn disabled) for respawning eda.py'
|
|
215
|
+
' using $OC_ROOT/bin'))
|
|
214
216
|
return parser
|
|
215
217
|
|
|
216
|
-
|
|
218
|
+
|
|
219
|
+
def get_argparser_short_help(parser: object = None) -> str:
|
|
220
|
+
'''Returns short help for our ArgumentParser'''
|
|
217
221
|
if not parser:
|
|
218
222
|
parser = get_argparser()
|
|
219
223
|
full_lines = parser.format_help().split('\n')
|
|
@@ -225,18 +229,25 @@ def get_argparser_short_help(parser=None) -> str:
|
|
|
225
229
|
return f'{parser.prog}:\n' + '\n'.join(full_lines[lineno + 1:])
|
|
226
230
|
|
|
227
231
|
|
|
228
|
-
def process_token(arg:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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}")
|
|
234
241
|
return True
|
|
235
242
|
return False
|
|
236
243
|
|
|
237
244
|
|
|
238
245
|
def process_tokens(tokens:list) -> (argparse.Namespace, list):
|
|
239
|
-
|
|
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
|
|
240
251
|
|
|
241
252
|
parser = get_argparser()
|
|
242
253
|
try:
|
|
@@ -245,251 +256,316 @@ def process_tokens(tokens:list) -> (argparse.Namespace, list):
|
|
|
245
256
|
except argparse.ArgumentError:
|
|
246
257
|
error(f'problem attempting to parse_known_args for {tokens=}')
|
|
247
258
|
|
|
248
|
-
if parsed.debug_level:
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
251
265
|
|
|
252
266
|
debug(f'util.process_tokens: {parsed=} {unparsed=} from {tokens=}')
|
|
253
267
|
|
|
254
|
-
if parsed.force_logfile
|
|
268
|
+
if parsed.force_logfile:
|
|
255
269
|
start_log(parsed.force_logfile, force=True)
|
|
256
|
-
elif parsed.logfile
|
|
270
|
+
elif parsed.logfile:
|
|
257
271
|
start_log(parsed.logfile, force=False)
|
|
272
|
+
elif parsed.default_log and \
|
|
273
|
+
(parsed.force_logfile is None and parsed.logfile is None):
|
|
274
|
+
# Use a forced logfile in the eda.work/eda.log:
|
|
275
|
+
start_log(global_log.default_log_filepath, force=True)
|
|
276
|
+
|
|
258
277
|
|
|
259
278
|
parsed_as_dict = vars(parsed)
|
|
260
279
|
for key,value in parsed_as_dict.items():
|
|
261
280
|
if value is not None:
|
|
262
281
|
args[key] = value # only update with non-None values to our global 'args' dict
|
|
263
|
-
return parsed, unparsed
|
|
264
282
|
|
|
265
|
-
|
|
266
|
-
"""Returns str, wraps text to a specified width and indents subsequent lines."""
|
|
267
|
-
wrapped_lines = textwrap.wrap(text, width=width,
|
|
268
|
-
initial_indent=' ' * initial_indent,
|
|
269
|
-
subsequent_indent=' ' * indent)
|
|
270
|
-
return '\n'.join(wrapped_lines)
|
|
283
|
+
return parsed, unparsed
|
|
271
284
|
|
|
272
285
|
# ********************
|
|
273
286
|
# fancy support
|
|
274
|
-
# In fancy mode, we take the bottom
|
|
275
|
-
# while the lines above that show regular scrolling content (via info, debug, warning,
|
|
276
|
-
# User should not use print() when in fancy mode
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def fancy_start(fancy_lines = 4, min_vanilla_lines = 4):
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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("")
|
|
291
310
|
print("", end="") # the last line has no "\n" because we don't want ANOTHER blank line below
|
|
292
|
-
|
|
311
|
+
_FANCY_LINES.append("")
|
|
293
312
|
# the cursor remains at the leftmost character of the bottom line of the screen
|
|
294
313
|
|
|
295
|
-
def fancy_stop():
|
|
296
|
-
|
|
297
|
-
if
|
|
298
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
|
|
307
333
|
# strip any newline, we don't want to print that
|
|
308
|
-
|
|
309
|
-
lines_above = len(
|
|
334
|
+
text = text.rstrip("\n")
|
|
335
|
+
lines_above = len(_FANCY_LINES) - line - 1
|
|
310
336
|
if lines_above:
|
|
311
|
-
print(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
+
)
|
|
315
344
|
else:
|
|
316
|
-
print(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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'''
|
|
321
353
|
# stuff we do before printing any line
|
|
322
|
-
if
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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'''
|
|
331
369
|
# stuff we do after printing any line
|
|
332
|
-
if
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
print(
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
|
|
341
379
|
print("\033[1G", end="", flush=True)
|
|
342
|
-
if
|
|
380
|
+
if global_log.file:
|
|
381
|
+
write_log(text, end=end)
|
|
343
382
|
|
|
344
|
-
string_red = f"\x1B[31m"
|
|
345
|
-
string_green = f"\x1B[32m"
|
|
346
|
-
string_orange = f"\x1B[33m"
|
|
347
|
-
string_yellow = f"\x1B[39m"
|
|
348
|
-
string_normal = f"\x1B[0m"
|
|
349
383
|
|
|
350
|
-
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]'''
|
|
351
386
|
print_pre()
|
|
352
|
-
print(
|
|
387
|
+
print(Colors.color_text(text, color), end=end, flush=True)
|
|
353
388
|
print_post(text, end)
|
|
354
389
|
|
|
355
|
-
def
|
|
390
|
+
def print_red(text: str, end: str = '\n') -> None:
|
|
391
|
+
'''Print text as red, goes back to normal color'''
|
|
356
392
|
print_pre()
|
|
357
|
-
print(
|
|
393
|
+
print(red_text(text), end=end, flush=True)
|
|
358
394
|
print_post(text, end)
|
|
359
395
|
|
|
360
|
-
def
|
|
396
|
+
def print_green(text: str, end: str = '\n') -> None:
|
|
397
|
+
'''Print text as green, goes back to normal color'''
|
|
361
398
|
print_pre()
|
|
362
|
-
print(
|
|
399
|
+
print(green_text(text), end=end, flush=True)
|
|
363
400
|
print_post(text, end)
|
|
364
401
|
|
|
365
|
-
def
|
|
402
|
+
def print_orange(text: str, end: str = '\n') -> None:
|
|
403
|
+
'''Print text as orange, goes back to normal color'''
|
|
366
404
|
print_pre()
|
|
367
|
-
print(
|
|
405
|
+
print(orange_text(text), end=end, flush=True)
|
|
368
406
|
print_post(text, end)
|
|
369
407
|
|
|
370
|
-
def
|
|
371
|
-
|
|
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
|
|
372
418
|
debug_level = level
|
|
373
|
-
args['debug'] =
|
|
374
|
-
args['verbose'] =
|
|
419
|
+
args['debug'] = level > 0
|
|
420
|
+
args['verbose'] = level > 1
|
|
375
421
|
info(f"Set debug level to {debug_level}")
|
|
376
422
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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)):
|
|
382
438
|
print_yellow(f"{start}{' '.join(list(text))}", end=end)
|
|
383
439
|
|
|
384
|
-
|
|
385
|
-
|
|
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 "")
|
|
386
452
|
if not args['quiet']:
|
|
387
453
|
print_green(f"{start}{' '.join(list(text))}", end=end)
|
|
388
454
|
|
|
389
|
-
def warning(*text, start=
|
|
390
|
-
|
|
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 "")
|
|
391
466
|
args['warnings'] += 1
|
|
392
467
|
print_orange(f"{start}{' '.join(list(text))}", end=end)
|
|
393
468
|
|
|
394
|
-
|
|
395
|
-
|
|
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 "")
|
|
396
486
|
args['errors'] += 1
|
|
397
487
|
print_red(f"{start}{' '.join(list(text))}", end=end)
|
|
398
488
|
if do_exit:
|
|
399
|
-
if args['debug']:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return 0
|
|
404
|
-
else:
|
|
405
|
-
return abs(int(error_code))
|
|
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
|
|
406
493
|
|
|
407
|
-
|
|
494
|
+
if error_code is None:
|
|
495
|
+
return 0
|
|
496
|
+
return abs(int(error_code))
|
|
497
|
+
|
|
498
|
+
|
|
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'''
|
|
408
503
|
if global_exit_allowed:
|
|
409
|
-
if not quiet:
|
|
504
|
+
if not quiet:
|
|
505
|
+
info(f"Exiting with {args['warnings']} warnings, {args['errors']} errors")
|
|
410
506
|
sys.exit(error_code)
|
|
411
507
|
|
|
412
508
|
if error_code is None:
|
|
413
509
|
return 0
|
|
414
|
-
|
|
415
|
-
|
|
510
|
+
|
|
511
|
+
return abs(int(error_code))
|
|
512
|
+
|
|
416
513
|
|
|
417
514
|
def getcwd():
|
|
515
|
+
'''Wrapper for os.getcwd() for current working dir'''
|
|
418
516
|
try:
|
|
419
|
-
|
|
517
|
+
return os.getcwd()
|
|
420
518
|
except Exception as e:
|
|
421
|
-
error("Unable to getcwd(), did it get deleted from under us?")
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
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:
|
|
429
526
|
'''Returns a str or False for the root directory of *this* repo.
|
|
430
527
|
|
|
431
528
|
If environment variable OC_ROOT is set, that is used instead, otherwise attempts to use
|
|
432
529
|
`git rev-parse`
|
|
433
530
|
'''
|
|
531
|
+
|
|
532
|
+
global _OC_ROOT # pylint: disable=global-statement
|
|
533
|
+
global _OC_ROOT_SET # pylint: disable=global-statement
|
|
434
534
|
# if we've already run through here once, just return the memorized result
|
|
435
|
-
if
|
|
535
|
+
if _OC_ROOT_SET:
|
|
536
|
+
return _OC_ROOT
|
|
436
537
|
|
|
437
538
|
# try looking for an env var
|
|
438
539
|
s = os.environ.get('OC_ROOT')
|
|
439
540
|
if s:
|
|
440
541
|
debug(f'get_oc_root() -- returning from env: {s=}')
|
|
441
|
-
|
|
542
|
+
_OC_ROOT = s.strip()
|
|
442
543
|
else:
|
|
443
544
|
# try asking GIT
|
|
444
|
-
cp = subprocess.run(
|
|
445
|
-
|
|
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
|
+
)
|
|
446
549
|
if cp.returncode != 0:
|
|
447
|
-
# TODO(drew): at some point, address the fact that not all repos are oc_root.
|
|
448
|
-
# 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?
|
|
449
553
|
print_didnt_find_it = debug
|
|
450
554
|
if error_on_fail:
|
|
451
555
|
print_didnt_find_it = error
|
|
452
|
-
print_didnt_find_it(
|
|
556
|
+
print_didnt_find_it('Unable to get a OC_ROOT directory using git rev-parse')
|
|
453
557
|
else:
|
|
454
|
-
|
|
455
|
-
if sys.platform
|
|
456
|
-
|
|
558
|
+
_OC_ROOT = cp.stdout.strip()
|
|
559
|
+
if sys.platform.startswith('win'):
|
|
560
|
+
_OC_ROOT = _OC_ROOT.replace('/', '\\') # git gives us /, but we need \
|
|
457
561
|
|
|
458
562
|
# there is no sense running through this code more than once
|
|
459
|
-
|
|
460
|
-
return
|
|
563
|
+
_OC_ROOT_SET = True
|
|
564
|
+
return _OC_ROOT
|
|
461
565
|
|
|
462
|
-
def string_or_space(text, whitespace=False):
|
|
463
|
-
if whitespace:
|
|
464
|
-
return " " * len(text)
|
|
465
|
-
else:
|
|
466
|
-
return text
|
|
467
566
|
|
|
468
|
-
def
|
|
469
|
-
|
|
470
|
-
txt = ""
|
|
471
|
-
do_all = False
|
|
472
|
-
# days
|
|
473
|
-
if (s >= (24*60*60)): # greater than 24h, we show days
|
|
474
|
-
d = int(s/(24*60*60))
|
|
475
|
-
txt += f"{d}d:"
|
|
476
|
-
s -= (d*24*60*60)
|
|
477
|
-
do_all = True
|
|
478
|
-
# hours
|
|
479
|
-
if do_all or (s >= (60*60)):
|
|
480
|
-
d = int(s/(60*60))
|
|
481
|
-
txt += f"{d:2}:"
|
|
482
|
-
s -= (d*60*60)
|
|
483
|
-
do_all = True
|
|
484
|
-
# minutes
|
|
485
|
-
d = int(s/(60))
|
|
486
|
-
txt += f"{d:02}:"
|
|
487
|
-
s -= (d*60)
|
|
488
|
-
# seconds
|
|
489
|
-
txt += f"{s:02}"
|
|
490
|
-
return txt
|
|
491
|
-
|
|
492
|
-
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'''
|
|
493
569
|
try:
|
|
494
570
|
# Infer if destination is a directory
|
|
495
571
|
if destination.endswith('/') or os.path.isdir(destination):
|
|
@@ -503,41 +579,45 @@ def safe_cp(source:str, destination:str, create_dirs:bool=False):
|
|
|
503
579
|
os.makedirs(parent_dir, exist_ok=True)
|
|
504
580
|
# actually copy the file
|
|
505
581
|
shutil.copy2(source, destination)
|
|
582
|
+
info(f"Copied {source} to {destination}")
|
|
506
583
|
except Exception as e:
|
|
507
584
|
print(f"Error copying file from '{source}' to '{destination}': {e}")
|
|
508
|
-
info(f"Copied {source} to {destination}")
|
|
509
585
|
|
|
510
|
-
def safe_rmdir(path):
|
|
511
|
-
"""Safely and reliably remove a non-empty directory."""
|
|
512
|
-
try:
|
|
513
|
-
# Ensure the path exists
|
|
514
|
-
if os.path.exists(path):
|
|
515
|
-
shutil.rmtree(path)
|
|
516
|
-
info(f"Directory '{path}' has been removed successfully.")
|
|
517
|
-
else:
|
|
518
|
-
debug(f"Directory '{path}' does not exist.")
|
|
519
|
-
except Exception as e:
|
|
520
|
-
error(f"An error occurred while removing the directory '{path}': {e}")
|
|
521
586
|
|
|
522
|
-
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'''
|
|
589
|
+
if os.path.exists(path):
|
|
590
|
+
return
|
|
591
|
+
left, _ = os.path.split(os.path.relpath(path))
|
|
592
|
+
if left and left not in ['.', '..', os.path.sep]:
|
|
593
|
+
safe_mkdir(left)
|
|
523
594
|
try:
|
|
524
595
|
os.mkdir(path)
|
|
525
596
|
except FileExistsError:
|
|
526
597
|
pass
|
|
527
|
-
except:
|
|
598
|
+
except Exception as e1:
|
|
528
599
|
try:
|
|
529
600
|
os.system(f'mkdir -p {path}')
|
|
530
|
-
except Exception as
|
|
531
|
-
error(f'unable to mkdir {path=},
|
|
601
|
+
except Exception as e2:
|
|
602
|
+
error(f'unable to mkdir {path=}, exceptions {e1}, {e2=}')
|
|
532
603
|
|
|
533
|
-
|
|
604
|
+
|
|
605
|
+
def safe_mkdirs(base : str, new_dirs : list) -> None:
|
|
606
|
+
'''Create new_dirs at base'''
|
|
534
607
|
for p in new_dirs:
|
|
535
608
|
safe_mkdir( os.path.join(base, p) )
|
|
536
609
|
|
|
537
610
|
|
|
538
|
-
def
|
|
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)
|
|
614
|
+
if left:
|
|
615
|
+
safe_mkdir(left)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def import_class_from_string(full_class_name: str) -> None:
|
|
539
619
|
"""
|
|
540
|
-
Imports a class given its full name as a
|
|
620
|
+
Imports a class given its full name as a str.
|
|
541
621
|
|
|
542
622
|
Args:
|
|
543
623
|
full_class_name: The full name of the class,
|
|
@@ -546,18 +626,21 @@ def import_class_from_string(full_class_name):
|
|
|
546
626
|
Returns:
|
|
547
627
|
The imported class, or None if an error occurs.
|
|
548
628
|
"""
|
|
549
|
-
from importlib import import_module
|
|
550
629
|
try:
|
|
551
630
|
module_path, class_name = full_class_name.rsplit(".", 1)
|
|
552
631
|
module = import_module(module_path)
|
|
553
632
|
return getattr(module, class_name)
|
|
554
|
-
except
|
|
555
|
-
print(f"Error importing class {full_class_name=}: {e
|
|
633
|
+
except Exception as e:
|
|
634
|
+
print(f"Error importing class {full_class_name=}: {e}")
|
|
556
635
|
return None
|
|
557
636
|
|
|
558
637
|
|
|
559
638
|
class ShellCommandList(list):
|
|
560
|
-
|
|
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 = ''):
|
|
561
644
|
super().__init__(obj)
|
|
562
645
|
for k in ['tee_fpath']:
|
|
563
646
|
setattr(self, k, getattr(obj, k, None))
|
|
@@ -565,29 +648,35 @@ class ShellCommandList(list):
|
|
|
565
648
|
self.tee_fpath = tee_fpath
|
|
566
649
|
|
|
567
650
|
|
|
568
|
-
def write_shell_command_file(
|
|
569
|
-
|
|
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)
|
|
570
656
|
|
|
571
657
|
-- dirpath (str) -- directory where file is written (usually eda.work/{target}_sim
|
|
572
658
|
-- filename (str) -- filename, for example compile_only.sh
|
|
573
|
-
-- command_lists (list) -- list of (list or ShellCommandList), each item in the list is a
|
|
574
|
-
subprocess.run(args) uses a list of
|
|
575
|
-
|
|
576
|
-
|
|
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.
|
|
577
665
|
|
|
578
666
|
Returns None, writes the file and chmod's it to 0x755.
|
|
579
|
-
|
|
580
667
|
'''
|
|
668
|
+
|
|
581
669
|
# command_lists should be a list-of-lists.
|
|
582
670
|
bash_path = shutil.which('bash')
|
|
583
|
-
assert
|
|
671
|
+
assert isinstance(command_lists, list), f'{command_lists=}'
|
|
584
672
|
fullpath = os.path.join(dirpath, filename)
|
|
585
|
-
with open(fullpath, 'w') as f:
|
|
673
|
+
with open(fullpath, 'w', encoding='utf-8') as f:
|
|
586
674
|
if not bash_path:
|
|
587
675
|
bash_path = "/bin/bash" # we may not get far, but we'll try
|
|
588
676
|
f.write('#!' + bash_path + '\n\n')
|
|
589
677
|
for obj in command_lists:
|
|
590
|
-
assert isinstance(obj, list),
|
|
678
|
+
assert isinstance(obj, list), \
|
|
679
|
+
f'{obj=} (obj must be list/ShellCommandList) {command_lists=}'
|
|
591
680
|
clist = list(obj).copy()
|
|
592
681
|
tee_fpath = getattr(obj, 'tee_fpath', None)
|
|
593
682
|
if tee_fpath:
|
|
@@ -599,7 +688,7 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
|
|
|
599
688
|
else:
|
|
600
689
|
clist.append(f'2>&1 | tee {tee_fpath}')
|
|
601
690
|
|
|
602
|
-
if
|
|
691
|
+
if clist:
|
|
603
692
|
if line_breaks:
|
|
604
693
|
# line_breaks=True - have 1 word per line, followed by \ and \n
|
|
605
694
|
sep = " \\" + "\n"
|
|
@@ -616,26 +705,6 @@ def write_shell_command_file(dirpath : str, filename : str, command_lists : list
|
|
|
616
705
|
os.chmod(fullpath, 0o755)
|
|
617
706
|
|
|
618
707
|
|
|
619
|
-
def write_eda_config_and_args(dirpath : str, filename=EDA_OUTPUT_CONFIG_FNAME, command_obj_ref=None):
|
|
620
|
-
import copy
|
|
621
|
-
if command_obj_ref is None:
|
|
622
|
-
return
|
|
623
|
-
fullpath = os.path.join(dirpath, filename)
|
|
624
|
-
data = dict()
|
|
625
|
-
for x in ['command_name', 'config', 'target', 'args', 'modified_args', 'defines',
|
|
626
|
-
'incdirs', 'files_v', 'files_sv', 'files_vhd']:
|
|
627
|
-
# Use deep copy b/c otherwise these are references to opencos.eda.
|
|
628
|
-
data[x] = copy.deepcopy(getattr(command_obj_ref, x, ''))
|
|
629
|
-
|
|
630
|
-
# fix some burried class references in command_obj_ref.config,
|
|
631
|
-
# otherwise we won't be able to safe load this yaml, so cast as str repr.
|
|
632
|
-
for k, v in command_obj_ref.config.items():
|
|
633
|
-
if k == 'command_handler':
|
|
634
|
-
data['config'][k] = str(v)
|
|
635
|
-
|
|
636
|
-
yaml_safe_writer(data=data, filepath=fullpath)
|
|
637
|
-
|
|
638
|
-
|
|
639
708
|
def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
|
|
640
709
|
'''Returns the best guess as the 'top' module name name, given a fpath where
|
|
641
710
|
|
|
@@ -660,121 +729,9 @@ def get_inferred_top_module_name(module_guess: str, module_fpath: str) -> str:
|
|
|
660
729
|
if bool(re.fullmatch(r'^\w+$', module_name)):
|
|
661
730
|
if module_name == module_guess:
|
|
662
731
|
return module_guess
|
|
663
|
-
|
|
732
|
+
if module_name:
|
|
664
733
|
best_guess = module_name
|
|
665
734
|
if best_guess:
|
|
666
735
|
return best_guess
|
|
667
|
-
else:
|
|
668
|
-
return ''
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
def subprocess_run(work_dir, command_list, fake:bool=False, shell=False) -> int:
|
|
672
|
-
''' Run command_list in the foreground, with preference to use bash if shell=True.'''
|
|
673
|
-
|
|
674
|
-
if work_dir is not None:
|
|
675
|
-
os.chdir(work_dir)
|
|
676
|
-
|
|
677
|
-
is_windows = sys.platform.startswith('win')
|
|
678
|
-
|
|
679
|
-
proc_kwargs = {'shell': shell}
|
|
680
|
-
bash_exec = shutil.which('bash')
|
|
681
|
-
if shell and bash_exec and not is_windows:
|
|
682
|
-
proc_kwargs.update({'executable': bash_exec})
|
|
683
|
-
|
|
684
|
-
if not is_windows and shell:
|
|
685
|
-
c = ' '.join(command_list)
|
|
686
|
-
else:
|
|
687
|
-
c = command_list
|
|
688
|
-
|
|
689
|
-
if fake:
|
|
690
|
-
info(f"util.subprocess_run FAKE: would have called subprocess.run({c}, **{proc_kwargs}")
|
|
691
|
-
return 0
|
|
692
|
-
else:
|
|
693
|
-
debug(f"util.subprocess_run: About to call subprocess.run({c}, **{proc_kwargs}")
|
|
694
|
-
proc = subprocess.run(c, **proc_kwargs)
|
|
695
|
-
return proc.returncode
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
def subprocess_run_background(work_dir, command_list, background=True, fake:bool=False,
|
|
699
|
-
shell=False, tee_fpath=None) -> (str, str, int):
|
|
700
|
-
''' Run command_list in the background, with preference to use bash if shell=True
|
|
701
|
-
|
|
702
|
-
tee_fpath is relative to work_dir.
|
|
703
|
-
'''
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
is_windows = sys.platform.startswith('win')
|
|
707
|
-
|
|
708
|
-
debug(f'util.subprocess_run_background: {background=} {tee_fpath=} {shell=}')
|
|
709
|
-
|
|
710
|
-
if fake or (not background and not tee_fpath):
|
|
711
|
-
# If tee_fpath is set, we're going to "background" but with it also
|
|
712
|
-
# printed out (stdout and tee_fpath).
|
|
713
|
-
rc = subprocess_run(work_dir, command_list, fake=fake, shell=shell)
|
|
714
|
-
return '', '', rc
|
|
715
|
-
|
|
716
|
-
if work_dir is not None:
|
|
717
|
-
os.chdir(work_dir)
|
|
718
|
-
|
|
719
|
-
proc_kwargs = {'shell': shell,
|
|
720
|
-
'stdout': subprocess.PIPE,
|
|
721
|
-
'stderr': subprocess.STDOUT,
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
bash_exec = shutil.which('bash')
|
|
725
|
-
if shell and bash_exec and not is_windows:
|
|
726
|
-
# Note - windows powershell will end up calling: /bin/bash /c, which won't work
|
|
727
|
-
proc_kwargs.update({'executable': bash_exec})
|
|
728
|
-
|
|
729
|
-
if not is_windows and shell:
|
|
730
|
-
c = ' '.join(command_list)
|
|
731
|
-
else:
|
|
732
|
-
c = command_list # leave as list.
|
|
733
|
-
|
|
734
|
-
debug(f"util.subprocess_run_background: about to call subprocess.Popen({c}, **{proc_kwargs})")
|
|
735
|
-
proc = subprocess.Popen(c, **proc_kwargs)
|
|
736
|
-
|
|
737
|
-
if tee_fpath:
|
|
738
|
-
stdout = ''
|
|
739
|
-
stderr = ''
|
|
740
|
-
with open(tee_fpath, 'w') as f:
|
|
741
|
-
for line in iter(proc.stdout.readline, b''):
|
|
742
|
-
line = line.rstrip().decode("utf-8", errors="replace")
|
|
743
|
-
if not background:
|
|
744
|
-
print(line)
|
|
745
|
-
f.write(line + '\n')
|
|
746
|
-
stdout += line + '\n'
|
|
747
|
-
|
|
748
|
-
proc.communicate()
|
|
749
|
-
rc = proc.returncode
|
|
750
|
-
info('util.subprocess_run_background: wrote: ' + os.path.abspath(tee_fpath))
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
else:
|
|
754
736
|
|
|
755
|
-
|
|
756
|
-
stdout, stderr = proc.communicate()
|
|
757
|
-
rc = proc.returncode
|
|
758
|
-
|
|
759
|
-
stdout = stdout.decode('utf-8', errors="replace") if stdout else ""
|
|
760
|
-
stderr = stderr.decode('utf-8', errors="replace") if stderr else ""
|
|
761
|
-
debug(f"shell_run_background: {rc=}")
|
|
762
|
-
if stdout:
|
|
763
|
-
for lineno, line in enumerate(stdout.strip().split('\n')):
|
|
764
|
-
debug(f"stdout:{lineno+1}: {line}")
|
|
765
|
-
if stderr:
|
|
766
|
-
for lineno, line in enumerate(stdout.strip().split('\n')):
|
|
767
|
-
debug(f"stderr:{lineno+1}: {line}")
|
|
768
|
-
|
|
769
|
-
return stdout, stderr, rc
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
def sanitize_defines_for_sh(value):
|
|
773
|
-
# Need to sanitize this for shell in case someone sends a +define+foo+1'b0,
|
|
774
|
-
# which needs to be escaped as +define+foo+1\'b0, otherwise bash or sh will
|
|
775
|
-
# think this is an unterminated string.
|
|
776
|
-
# TODO(drew): decide if we should instead us shlex.quote('+define+key=value')
|
|
777
|
-
# instead of this function.
|
|
778
|
-
if type(value) is str:
|
|
779
|
-
value = value.replace("'", "\\" + "'")
|
|
780
|
-
return value
|
|
737
|
+
return ''
|