shrinkwrap-tool 2026.2.1__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.
- shrinkwrap/__init__.py +1 -0
- shrinkwrap/__main__.py +4 -0
- shrinkwrap/commands/__init__.py +0 -0
- shrinkwrap/commands/build.py +91 -0
- shrinkwrap/commands/buildall.py +180 -0
- shrinkwrap/commands/clean.py +161 -0
- shrinkwrap/commands/inspect.py +235 -0
- shrinkwrap/commands/process.py +106 -0
- shrinkwrap/commands/run.py +311 -0
- shrinkwrap/config/FVP_Base_RevC-2xAEMvA-base.yaml +98 -0
- shrinkwrap/config/FVP_Base_RevC-2xAEMvA-rme.yaml +42 -0
- shrinkwrap/config/arch/v8.0.yaml +22 -0
- shrinkwrap/config/arch/v8.1.yaml +26 -0
- shrinkwrap/config/arch/v8.2.yaml +28 -0
- shrinkwrap/config/arch/v8.3.yaml +25 -0
- shrinkwrap/config/arch/v8.4.yaml +26 -0
- shrinkwrap/config/arch/v8.5.yaml +29 -0
- shrinkwrap/config/arch/v8.6.yaml +28 -0
- shrinkwrap/config/arch/v8.7.yaml +24 -0
- shrinkwrap/config/arch/v8.8.yaml +31 -0
- shrinkwrap/config/arch/v8.9.yaml +32 -0
- shrinkwrap/config/arch/v9.0.yaml +29 -0
- shrinkwrap/config/arch/v9.1.yaml +25 -0
- shrinkwrap/config/arch/v9.2.yaml +29 -0
- shrinkwrap/config/arch/v9.3.yaml +23 -0
- shrinkwrap/config/arch/v9.4.yaml +21 -0
- shrinkwrap/config/arch/v9.5.yaml +20 -0
- shrinkwrap/config/bootwrapper.yaml +76 -0
- shrinkwrap/config/buildroot-cca.yaml +113 -0
- shrinkwrap/config/buildroot.yaml +54 -0
- shrinkwrap/config/cca-3world.yaml +215 -0
- shrinkwrap/config/cca-4world.yaml +57 -0
- shrinkwrap/config/cca-edk2.yaml +58 -0
- shrinkwrap/config/debug/rmm.yaml +15 -0
- shrinkwrap/config/debug/tfa.yaml +18 -0
- shrinkwrap/config/debug/tftf.yaml +17 -0
- shrinkwrap/config/dt-base.yaml +115 -0
- shrinkwrap/config/edk2-base.yaml +59 -0
- shrinkwrap/config/ffa-hafnium-optee.yaml +45 -0
- shrinkwrap/config/ffa-optee.yaml +30 -0
- shrinkwrap/config/ffa-tftf.yaml +26 -0
- shrinkwrap/config/hafnium-base.yaml +51 -0
- shrinkwrap/config/kvm-unit-tests.yaml +32 -0
- shrinkwrap/config/kvmtool-base.yaml +33 -0
- shrinkwrap/config/linux-base.yaml +80 -0
- shrinkwrap/config/ns-edk2-base.yaml +83 -0
- shrinkwrap/config/ns-edk2-optee.yaml +41 -0
- shrinkwrap/config/ns-edk2.yaml +49 -0
- shrinkwrap/config/ns-preload.yaml +98 -0
- shrinkwrap/config/optee-base.yaml +37 -0
- shrinkwrap/config/rfa-base.yaml +49 -0
- shrinkwrap/config/rfa.yaml +47 -0
- shrinkwrap/config/rmm-base.yaml +24 -0
- shrinkwrap/config/rust.yaml +31 -0
- shrinkwrap/config/test/cca.yaml +47 -0
- shrinkwrap/config/tfa-base.yaml +45 -0
- shrinkwrap/config/tfa-rme.yaml +36 -0
- shrinkwrap/config/tftf-base.yaml +32 -0
- shrinkwrap/shrinkwrap_main.py +133 -0
- shrinkwrap/utils/__init__.py +0 -0
- shrinkwrap/utils/clivars.py +16 -0
- shrinkwrap/utils/config.py +1166 -0
- shrinkwrap/utils/graph.py +263 -0
- shrinkwrap/utils/label.py +153 -0
- shrinkwrap/utils/logger.py +160 -0
- shrinkwrap/utils/process.py +230 -0
- shrinkwrap/utils/runtime.py +192 -0
- shrinkwrap/utils/ssh_agent.py +98 -0
- shrinkwrap/utils/tty.py +46 -0
- shrinkwrap/utils/vars.py +14 -0
- shrinkwrap/utils/workspace.py +59 -0
- shrinkwrap_tool-2026.2.1.dist-info/METADATA +63 -0
- shrinkwrap_tool-2026.2.1.dist-info/RECORD +77 -0
- shrinkwrap_tool-2026.2.1.dist-info/WHEEL +5 -0
- shrinkwrap_tool-2026.2.1.dist-info/entry_points.txt +2 -0
- shrinkwrap_tool-2026.2.1.dist-info/licenses/license.rst +41 -0
- shrinkwrap_tool-2026.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Copyright (c) 2022, Arm Limited.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import graphlib
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import shrinkwrap.utils.logger as logger
|
|
9
|
+
import shrinkwrap.utils.label as label
|
|
10
|
+
import shrinkwrap.utils.process as process
|
|
11
|
+
import shrinkwrap.utils.workspace as workspace
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _mk_labels(graph):
|
|
15
|
+
"""
|
|
16
|
+
Returns a tuple of labels and mask. Each is a two level dictionary,
|
|
17
|
+
where there is an entry for each config/component that we are building.
|
|
18
|
+
Mask is initially set to true.
|
|
19
|
+
"""
|
|
20
|
+
labels = {}
|
|
21
|
+
mask = {}
|
|
22
|
+
|
|
23
|
+
ts = graphlib.TopologicalSorter(graph)
|
|
24
|
+
ts.prepare()
|
|
25
|
+
while ts.is_active():
|
|
26
|
+
for frag in ts.get_ready():
|
|
27
|
+
ts.done(frag)
|
|
28
|
+
|
|
29
|
+
cfg = frag.config
|
|
30
|
+
cmp = frag.component
|
|
31
|
+
|
|
32
|
+
if cfg is None or cmp is None:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
if cfg not in labels:
|
|
36
|
+
labels[cfg] = {}
|
|
37
|
+
mask[cfg] = {}
|
|
38
|
+
if cmp not in labels[cfg]:
|
|
39
|
+
labels[cfg][cmp] = label.Label()
|
|
40
|
+
mask[cfg][cmp] = True
|
|
41
|
+
|
|
42
|
+
return labels, mask
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mk_label_controller(labels, overdraw):
|
|
46
|
+
"""
|
|
47
|
+
Makes a label controller for all the labels in the labels two-level
|
|
48
|
+
dictionary.
|
|
49
|
+
"""
|
|
50
|
+
label_list = []
|
|
51
|
+
for sl in labels.values():
|
|
52
|
+
for l in sl.values():
|
|
53
|
+
label_list.append(l)
|
|
54
|
+
return label.LabelController(label_list, overdraw=overdraw)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _mk_tag(config, component):
|
|
58
|
+
"""
|
|
59
|
+
Makes a fixed-width tag string for a config and component.
|
|
60
|
+
"""
|
|
61
|
+
def _clamp(text, max):
|
|
62
|
+
if len(text) > max:
|
|
63
|
+
text = text[:max-3] + '...'
|
|
64
|
+
return text
|
|
65
|
+
|
|
66
|
+
config = '' if config is None else config
|
|
67
|
+
component = '' if component is None else component
|
|
68
|
+
config = _clamp(config, 10)
|
|
69
|
+
component = _clamp(component, 14)
|
|
70
|
+
return '[ {:>10} : {:14} ]'.format(config, component)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _update_labels(labels, mask, config, component, summary):
|
|
74
|
+
"""
|
|
75
|
+
Updates all the labels whose mask is True and which match config and
|
|
76
|
+
component (if specified, then only update for that config/component. If
|
|
77
|
+
None, then update all configs/components). summary provides the text to
|
|
78
|
+
update the labels with.
|
|
79
|
+
"""
|
|
80
|
+
def iter(labels, mask, key):
|
|
81
|
+
if key is None:
|
|
82
|
+
for key in labels:
|
|
83
|
+
yield key, labels[key], mask[key]
|
|
84
|
+
else:
|
|
85
|
+
yield key, labels[key], mask[key]
|
|
86
|
+
|
|
87
|
+
for cfg, l0, m0 in iter(labels, mask, config):
|
|
88
|
+
for cmp, l1, m1 in iter(l0, m0, component):
|
|
89
|
+
if m1:
|
|
90
|
+
l1.update(_mk_tag(cfg, cmp) + ' ' + summary)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _run_script(pm, data, script):
|
|
94
|
+
# Write the script out to a file in a temp directory, and wrap the
|
|
95
|
+
# directory name and command to run in a Process. Add the Process to the
|
|
96
|
+
# ProcessManager. On completion, the caller must destroy the directory.
|
|
97
|
+
|
|
98
|
+
tmpdir = tempfile.mkdtemp(dir=workspace.build)
|
|
99
|
+
tmpfilename = os.path.join(tmpdir, 'script.sh')
|
|
100
|
+
with open(tmpfilename, 'w') as tmpfile:
|
|
101
|
+
tmpfile.write(script.commands())
|
|
102
|
+
|
|
103
|
+
# Start the process asynchronously.
|
|
104
|
+
pm.add(process.Process(f'bash {tmpfilename}',
|
|
105
|
+
False,
|
|
106
|
+
(*data, script, tmpdir),
|
|
107
|
+
True))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def execute(graph, tasks, verbose=False, colorize=True):
|
|
111
|
+
labels, mask = _mk_labels(graph)
|
|
112
|
+
lc = _mk_label_controller(labels, not verbose)
|
|
113
|
+
|
|
114
|
+
queue = []
|
|
115
|
+
active = 0
|
|
116
|
+
log = logger.Logger(27)
|
|
117
|
+
ts = graphlib.TopologicalSorter(graph)
|
|
118
|
+
lognum = {}
|
|
119
|
+
exec_error = None
|
|
120
|
+
|
|
121
|
+
def _pump(pm):
|
|
122
|
+
nonlocal queue
|
|
123
|
+
nonlocal active
|
|
124
|
+
nonlocal log
|
|
125
|
+
nonlocal lognum
|
|
126
|
+
while len(queue) > 0 and active < tasks:
|
|
127
|
+
frag = queue.pop()
|
|
128
|
+
logname = None
|
|
129
|
+
if frag.config and frag.component:
|
|
130
|
+
if frag.component not in lognum:
|
|
131
|
+
lognum[frag.component] = 0
|
|
132
|
+
logname = os.path.join(workspace.build,
|
|
133
|
+
'log',
|
|
134
|
+
frag.config,
|
|
135
|
+
f'{frag.component}{lognum[frag.component]}.log')
|
|
136
|
+
os.makedirs(os.path.dirname(logname), exist_ok=True)
|
|
137
|
+
lognum[frag.component] += 1
|
|
138
|
+
_update_labels(labels,
|
|
139
|
+
mask,
|
|
140
|
+
frag.config,
|
|
141
|
+
frag.component,
|
|
142
|
+
frag.summary + '...')
|
|
143
|
+
data = (log.alloc_data(str(frag), colorize, False, logname), [])
|
|
144
|
+
_run_script(pm, data, frag)
|
|
145
|
+
active += 1
|
|
146
|
+
|
|
147
|
+
def _should_log(proc, data, streamid):
|
|
148
|
+
if verbose or \
|
|
149
|
+
(streamid == process.STDERR and \
|
|
150
|
+
(not proc.data[2].stderrfilt or \
|
|
151
|
+
'warning' in data or 'error' in data)):
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def _log(pm, proc, data, streamid):
|
|
156
|
+
logstd = _should_log(proc, data, streamid)
|
|
157
|
+
if not verbose:
|
|
158
|
+
proc.data[1].append(data)
|
|
159
|
+
if logstd:
|
|
160
|
+
lc.erase()
|
|
161
|
+
log.log(pm, proc, data, streamid, logstd)
|
|
162
|
+
if logstd:
|
|
163
|
+
lc.update()
|
|
164
|
+
|
|
165
|
+
def _complete(pm, proc, retcode):
|
|
166
|
+
nonlocal queue
|
|
167
|
+
nonlocal active
|
|
168
|
+
nonlocal ts
|
|
169
|
+
nonlocal exec_error
|
|
170
|
+
|
|
171
|
+
data = proc.data[0]
|
|
172
|
+
err = proc.data[1]
|
|
173
|
+
frag = proc.data[2]
|
|
174
|
+
tmpdir = proc.data[3]
|
|
175
|
+
|
|
176
|
+
log.free_data(data)
|
|
177
|
+
shutil.rmtree(tmpdir)
|
|
178
|
+
|
|
179
|
+
if retcode is None:
|
|
180
|
+
# Forcibly terminated due to errors elsewhere. No need
|
|
181
|
+
# to do anything further.
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if retcode:
|
|
186
|
+
# Fatal error. Do not execute any new fragment, only wait for those
|
|
187
|
+
# that are currently running.
|
|
188
|
+
exec_error = {
|
|
189
|
+
"exception": Exception(f"Failed to execute '{frag}'"),
|
|
190
|
+
"error": ''.join(err),
|
|
191
|
+
}
|
|
192
|
+
queue = []
|
|
193
|
+
state = 'Error'
|
|
194
|
+
else:
|
|
195
|
+
state = 'Done' if frag.final else 'Waiting...'
|
|
196
|
+
|
|
197
|
+
_update_labels(labels,
|
|
198
|
+
mask,
|
|
199
|
+
frag.config,
|
|
200
|
+
frag.component,
|
|
201
|
+
state)
|
|
202
|
+
if frag.final:
|
|
203
|
+
mask[frag.config][frag.component] = False
|
|
204
|
+
|
|
205
|
+
active -= 1
|
|
206
|
+
if not exec_error:
|
|
207
|
+
ts.done(frag)
|
|
208
|
+
queue.extend(ts.get_ready())
|
|
209
|
+
_pump(pm)
|
|
210
|
+
|
|
211
|
+
lc.update()
|
|
212
|
+
|
|
213
|
+
# Initially set all labels to waiting. They will be updated as the
|
|
214
|
+
# fragments execute.
|
|
215
|
+
_update_labels(labels, mask, None, None, 'Waiting...')
|
|
216
|
+
|
|
217
|
+
# The process manager will run all added processes in the background and
|
|
218
|
+
# give callbacks whenever there is output available and when each
|
|
219
|
+
# process terminates. _pump() adds processes to the set.
|
|
220
|
+
pm = process.ProcessManager(_log, _complete)
|
|
221
|
+
|
|
222
|
+
# Fill the queue with all the initial script fragments which do not have
|
|
223
|
+
# start dependencies.
|
|
224
|
+
ts.prepare()
|
|
225
|
+
queue.extend(ts.get_ready())
|
|
226
|
+
|
|
227
|
+
# Call _pump() initially to start as many tasks as are allowed.
|
|
228
|
+
# Then enter the pm.
|
|
229
|
+
_pump(pm)
|
|
230
|
+
lc.update()
|
|
231
|
+
pm.run()
|
|
232
|
+
|
|
233
|
+
if exec_error:
|
|
234
|
+
if not verbose:
|
|
235
|
+
print('\n== error start ' + ('=' * 65))
|
|
236
|
+
print(exec_error['error'])
|
|
237
|
+
print('== error end ' + ('=' * 67) + '\n')
|
|
238
|
+
raise exec_error['exception']
|
|
239
|
+
|
|
240
|
+
# Mark all components as done. This should be a nop since the script
|
|
241
|
+
# should have indicated if it was the last step for a given
|
|
242
|
+
# config/component and we would have already set it to done. But this
|
|
243
|
+
# catches anything that might have slipped through.
|
|
244
|
+
_update_labels(labels, mask, None, None, 'Done')
|
|
245
|
+
lc.update()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def make_script(graph):
|
|
249
|
+
# Start the script with the preamble from the first script fragment in
|
|
250
|
+
# the graph. The preamble for each script is identical and we only need
|
|
251
|
+
# it once since we are concatenating the fragments together.
|
|
252
|
+
script = '' + list(graph.keys())[0].preamble() + '\n'
|
|
253
|
+
|
|
254
|
+
# Walk the graph, adding each script fragment to the final script
|
|
255
|
+
# (without its preamble).
|
|
256
|
+
ts = graphlib.TopologicalSorter(graph)
|
|
257
|
+
ts.prepare()
|
|
258
|
+
while ts.is_active():
|
|
259
|
+
for frag in ts.get_ready():
|
|
260
|
+
script += frag.commands(False) + '\n'
|
|
261
|
+
ts.done(frag)
|
|
262
|
+
|
|
263
|
+
return script
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Copyright (c) 2022, Arm Limited.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import shrinkwrap.utils.tty as tty
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Label:
|
|
11
|
+
"""
|
|
12
|
+
Container for a string. Intended to be used with LabelController to
|
|
13
|
+
display labels (strings of text) on a terminal at a fixed location and
|
|
14
|
+
be able to update them in place. Instantiate with desired text, and
|
|
15
|
+
update the text by calling update(). Nothing actually changes on screen
|
|
16
|
+
until the LabelController does an update().
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, text=''):
|
|
19
|
+
self.text = text
|
|
20
|
+
self._prev_text = ''
|
|
21
|
+
self._lc = None
|
|
22
|
+
|
|
23
|
+
def update(self, text=''):
|
|
24
|
+
self.text = text
|
|
25
|
+
if self._lc:
|
|
26
|
+
self._lc._update_pending = True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LabelController:
|
|
30
|
+
"""
|
|
31
|
+
Coordinates printing and updating a set of labels at a fixed location on
|
|
32
|
+
a terminal. The controller manages a list of labels, which are provided
|
|
33
|
+
at construction and are displayed one under the other on the terminal.
|
|
34
|
+
update() can be periodically called to redraw the labels if their
|
|
35
|
+
contents has changed. Only works as long as no other text is printed to
|
|
36
|
+
the terminal while the labels are active. If the provided file is not
|
|
37
|
+
backed by a terminal, overdrawing is not performed.
|
|
38
|
+
"""
|
|
39
|
+
def __init__(self, labels=[], overdraw=True):
|
|
40
|
+
self._labels = labels
|
|
41
|
+
self._overdraw = overdraw
|
|
42
|
+
self._drawn = False
|
|
43
|
+
self._cursor_col = 1
|
|
44
|
+
self._separator = ''
|
|
45
|
+
self._update_pending = True
|
|
46
|
+
|
|
47
|
+
self._out = sys.stdout
|
|
48
|
+
self._in = sys.stdin
|
|
49
|
+
|
|
50
|
+
if overdraw and not self._term_cols():
|
|
51
|
+
self._overdraw = False
|
|
52
|
+
|
|
53
|
+
for label in self._labels:
|
|
54
|
+
label._lc = self
|
|
55
|
+
|
|
56
|
+
def _term_cols(self):
|
|
57
|
+
try:
|
|
58
|
+
term_sz = os.get_terminal_size(self._out.fileno())
|
|
59
|
+
return term_sz.columns
|
|
60
|
+
except OSError:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def cursor_pos(self):
|
|
64
|
+
orig = tty.configure(self._in)
|
|
65
|
+
if orig:
|
|
66
|
+
try:
|
|
67
|
+
pos = ''
|
|
68
|
+
self._out.write('\x1b[6n')
|
|
69
|
+
self._out.flush()
|
|
70
|
+
while not (pos := pos + self._in.read(1)).endswith('R'):
|
|
71
|
+
pass
|
|
72
|
+
res = re.match(r'.*\[(?P<y>\d*);(?P<x>\d*)R', pos)
|
|
73
|
+
finally:
|
|
74
|
+
tty.restore(self._in, orig)
|
|
75
|
+
if(res):
|
|
76
|
+
return (int(res.group("x")), int(res.group("y")))
|
|
77
|
+
assert(False)
|
|
78
|
+
|
|
79
|
+
def _move_up(self, line_count, column=1):
|
|
80
|
+
assert(self._overdraw)
|
|
81
|
+
self._out.write(f'\033[{line_count}F')
|
|
82
|
+
if column > 1:
|
|
83
|
+
self._out.write(f'\033[{column}G')
|
|
84
|
+
|
|
85
|
+
def _line_count(self, text, term_cols):
|
|
86
|
+
assert(self._overdraw)
|
|
87
|
+
return (len(text) + term_cols - 1) // term_cols
|
|
88
|
+
|
|
89
|
+
def update(self):
|
|
90
|
+
if not self._update_pending:
|
|
91
|
+
return
|
|
92
|
+
if self._overdraw:
|
|
93
|
+
term_cols = self._term_cols()
|
|
94
|
+
if not self._drawn:
|
|
95
|
+
self._cursor_col = self.cursor_pos()[0]
|
|
96
|
+
if self._cursor_col > 1:
|
|
97
|
+
self._out.write('\n')
|
|
98
|
+
self._separator = '-' * term_cols
|
|
99
|
+
self._out.write(self._separator)
|
|
100
|
+
self._out.write('\n')
|
|
101
|
+
for l in self._labels:
|
|
102
|
+
self._out.write(l.text)
|
|
103
|
+
self._out.write('\n')
|
|
104
|
+
l._prev_text = l.text
|
|
105
|
+
else:
|
|
106
|
+
erase_lines = 0
|
|
107
|
+
for l in self._labels:
|
|
108
|
+
erase_lines += self._line_count(l._prev_text, term_cols)
|
|
109
|
+
self._move_up(erase_lines)
|
|
110
|
+
for l in self._labels:
|
|
111
|
+
cc = len(l.text)
|
|
112
|
+
pcc = len(l._prev_text)
|
|
113
|
+
self._out.write(l.text)
|
|
114
|
+
self._out.write(' ' * (pcc - cc))
|
|
115
|
+
self._out.write('\n')
|
|
116
|
+
l._prev_text = l.text
|
|
117
|
+
else:
|
|
118
|
+
for l in self._labels:
|
|
119
|
+
if l.text != l._prev_text:
|
|
120
|
+
self._out.write(l.text)
|
|
121
|
+
self._out.write('\n')
|
|
122
|
+
l._prev_text = l.text
|
|
123
|
+
self._out.flush()
|
|
124
|
+
self._drawn = True
|
|
125
|
+
self._update_pending = False
|
|
126
|
+
|
|
127
|
+
def erase(self):
|
|
128
|
+
if not self._overdraw or not self._drawn:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
term_cols = self._term_cols()
|
|
132
|
+
|
|
133
|
+
erase_lines = self._line_count(self._separator, term_cols)
|
|
134
|
+
for l in self._labels:
|
|
135
|
+
erase_lines += self._line_count(l._prev_text, term_cols)
|
|
136
|
+
|
|
137
|
+
self._move_up(erase_lines)
|
|
138
|
+
|
|
139
|
+
self._out.write(' ' * len(self._separator))
|
|
140
|
+
self._out.write('\n')
|
|
141
|
+
|
|
142
|
+
for l in self._labels:
|
|
143
|
+
pcc = len(l._prev_text)
|
|
144
|
+
self._out.write(' ' * pcc)
|
|
145
|
+
self._out.write('\n')
|
|
146
|
+
|
|
147
|
+
if self._cursor_col > 1:
|
|
148
|
+
erase_lines += 1
|
|
149
|
+
self._move_up(erase_lines, self._cursor_col)
|
|
150
|
+
|
|
151
|
+
self._out.flush()
|
|
152
|
+
self._drawn = False
|
|
153
|
+
self._update_pending = True
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Copyright (c) 2022, Arm Limited.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from collections import namedtuple
|
|
6
|
+
import re
|
|
7
|
+
termcolor = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _import_termcolor():
|
|
11
|
+
global termcolor
|
|
12
|
+
import termcolor as tc
|
|
13
|
+
termcolor = tc
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
17
|
+
_colors = ['blue', 'cyan', 'green', 'yellow', 'magenta']
|
|
18
|
+
Data = namedtuple("Data", "id tag color noesc escbuf logfile")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MatchBuf:
|
|
22
|
+
def __init__(self, match):
|
|
23
|
+
self._match = match
|
|
24
|
+
self._buf = ''
|
|
25
|
+
|
|
26
|
+
def match(self, data):
|
|
27
|
+
self._buf += data
|
|
28
|
+
found = self._buf.find(self._match) >= 0
|
|
29
|
+
self._buf = self._buf[1 - len(self._match):]
|
|
30
|
+
return found
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def splitlines(string):
|
|
34
|
+
"""
|
|
35
|
+
Like str.splitlines(True) but preserves '\r'.
|
|
36
|
+
"""
|
|
37
|
+
lines = string.split('\n')
|
|
38
|
+
keepends = [l + '\n' for l in lines[:-1]]
|
|
39
|
+
if lines[-1] != '':
|
|
40
|
+
keepends.append(lines[-1])
|
|
41
|
+
return keepends
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Logger:
|
|
45
|
+
def __init__(self, tag_size):
|
|
46
|
+
self._tag_size = tag_size
|
|
47
|
+
self._id_next = 0
|
|
48
|
+
self._color_next = 0
|
|
49
|
+
self._prev_id = None
|
|
50
|
+
self._prev_char = '\n'
|
|
51
|
+
|
|
52
|
+
def alloc_data(self, tag, colorize, no_escapes=False, logname=None):
|
|
53
|
+
"""
|
|
54
|
+
Returns the object that should be stashed in proc.data[0] when
|
|
55
|
+
log() is called. Includes the tag for the process and an
|
|
56
|
+
allocated colour.
|
|
57
|
+
"""
|
|
58
|
+
if colorize:
|
|
59
|
+
_import_termcolor()
|
|
60
|
+
color = self._color_next
|
|
61
|
+
self._color_next += 1
|
|
62
|
+
color = _colors[color % len(_colors)]
|
|
63
|
+
else:
|
|
64
|
+
color = None
|
|
65
|
+
|
|
66
|
+
id = self._id_next
|
|
67
|
+
self._id_next += 1
|
|
68
|
+
|
|
69
|
+
if isinstance(no_escapes, str):
|
|
70
|
+
noesc = True
|
|
71
|
+
escbuf = MatchBuf(no_escapes)
|
|
72
|
+
else:
|
|
73
|
+
noesc = no_escapes
|
|
74
|
+
escbuf = None
|
|
75
|
+
|
|
76
|
+
if logname:
|
|
77
|
+
logfile = open(logname, 'w', buffering=1)
|
|
78
|
+
else:
|
|
79
|
+
logfile = None
|
|
80
|
+
|
|
81
|
+
return Data(id, tag, color, [noesc], escbuf, logfile)
|
|
82
|
+
|
|
83
|
+
def free_data(self, data):
|
|
84
|
+
if data.logfile:
|
|
85
|
+
data.logfile.close()
|
|
86
|
+
|
|
87
|
+
def log(self, pm, proc, data, streamid, logstd=True):
|
|
88
|
+
"""
|
|
89
|
+
Logs text data from one of the processes (FVP or one of its uart
|
|
90
|
+
terminals) to the terminal. Text is colored and a tag is added
|
|
91
|
+
on the left to identify the originating process.
|
|
92
|
+
"""
|
|
93
|
+
id = proc.data[0].id
|
|
94
|
+
tag = proc.data[0].tag
|
|
95
|
+
color = proc.data[0].color
|
|
96
|
+
noesc = proc.data[0].noesc
|
|
97
|
+
escbuf = proc.data[0].escbuf
|
|
98
|
+
logfile = proc.data[0].logfile
|
|
99
|
+
|
|
100
|
+
# Write out to file if requested.
|
|
101
|
+
if logfile:
|
|
102
|
+
logfile.write(data)
|
|
103
|
+
|
|
104
|
+
# Write to stdout if requested.
|
|
105
|
+
if not logstd:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Remove any ansi escape sequences if requested.
|
|
109
|
+
if noesc[0]:
|
|
110
|
+
if escbuf and escbuf.match(data):
|
|
111
|
+
noesc[0] = False
|
|
112
|
+
else:
|
|
113
|
+
data = _ansi_escape.sub('', data)
|
|
114
|
+
if len(data) == 0:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Make the tag.
|
|
118
|
+
if len(tag) > self._tag_size:
|
|
119
|
+
tag = tag[:self._tag_size-3] + '...'
|
|
120
|
+
if len(tag):
|
|
121
|
+
tag = f'{{:>{self._tag_size}}}'.format(tag)
|
|
122
|
+
|
|
123
|
+
lines = splitlines(data)
|
|
124
|
+
start = 0
|
|
125
|
+
|
|
126
|
+
# If the cursor is not at the start of a new line, then if the
|
|
127
|
+
# new log is for the same proc that owns the first part of the
|
|
128
|
+
# line, just continue from there without adding a new tag. If
|
|
129
|
+
# the first part of the line has a different owner, insert a
|
|
130
|
+
# newline and add a tag for the new owner.
|
|
131
|
+
if self._prev_char != '\n':
|
|
132
|
+
if self._prev_id == id:
|
|
133
|
+
self.print(lines[0], tag, True, color, end='')
|
|
134
|
+
start = 1
|
|
135
|
+
else:
|
|
136
|
+
self.print('\n', tag, True, color, end='')
|
|
137
|
+
|
|
138
|
+
for line in lines[start:]:
|
|
139
|
+
self.print(line, tag, False, color, end='')
|
|
140
|
+
|
|
141
|
+
self._prev_id = id
|
|
142
|
+
self._prev_char = lines[-1][-1]
|
|
143
|
+
|
|
144
|
+
sys.stdout.flush()
|
|
145
|
+
|
|
146
|
+
def print(self, text, tag, cont, color=None, on_color=None, attrs=None, **kwargs):
|
|
147
|
+
# Ensure that any '\r's only rewind to the end of the tag.
|
|
148
|
+
if len(tag):
|
|
149
|
+
tag = f'[ {tag} ] '
|
|
150
|
+
text = text.replace('\r', f'\r{tag}')
|
|
151
|
+
|
|
152
|
+
if not cont:
|
|
153
|
+
self._print(tag, color, on_color, attrs, end='')
|
|
154
|
+
|
|
155
|
+
self._print(text, color, on_color, attrs, **kwargs)
|
|
156
|
+
|
|
157
|
+
def _print(self, text, color=None, on_color=None, attrs=None, **kwargs):
|
|
158
|
+
if color:
|
|
159
|
+
text = termcolor.colored(text, color, on_color, attrs)
|
|
160
|
+
print(text, **kwargs)
|