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.
Files changed (77) hide show
  1. shrinkwrap/__init__.py +1 -0
  2. shrinkwrap/__main__.py +4 -0
  3. shrinkwrap/commands/__init__.py +0 -0
  4. shrinkwrap/commands/build.py +91 -0
  5. shrinkwrap/commands/buildall.py +180 -0
  6. shrinkwrap/commands/clean.py +161 -0
  7. shrinkwrap/commands/inspect.py +235 -0
  8. shrinkwrap/commands/process.py +106 -0
  9. shrinkwrap/commands/run.py +311 -0
  10. shrinkwrap/config/FVP_Base_RevC-2xAEMvA-base.yaml +98 -0
  11. shrinkwrap/config/FVP_Base_RevC-2xAEMvA-rme.yaml +42 -0
  12. shrinkwrap/config/arch/v8.0.yaml +22 -0
  13. shrinkwrap/config/arch/v8.1.yaml +26 -0
  14. shrinkwrap/config/arch/v8.2.yaml +28 -0
  15. shrinkwrap/config/arch/v8.3.yaml +25 -0
  16. shrinkwrap/config/arch/v8.4.yaml +26 -0
  17. shrinkwrap/config/arch/v8.5.yaml +29 -0
  18. shrinkwrap/config/arch/v8.6.yaml +28 -0
  19. shrinkwrap/config/arch/v8.7.yaml +24 -0
  20. shrinkwrap/config/arch/v8.8.yaml +31 -0
  21. shrinkwrap/config/arch/v8.9.yaml +32 -0
  22. shrinkwrap/config/arch/v9.0.yaml +29 -0
  23. shrinkwrap/config/arch/v9.1.yaml +25 -0
  24. shrinkwrap/config/arch/v9.2.yaml +29 -0
  25. shrinkwrap/config/arch/v9.3.yaml +23 -0
  26. shrinkwrap/config/arch/v9.4.yaml +21 -0
  27. shrinkwrap/config/arch/v9.5.yaml +20 -0
  28. shrinkwrap/config/bootwrapper.yaml +76 -0
  29. shrinkwrap/config/buildroot-cca.yaml +113 -0
  30. shrinkwrap/config/buildroot.yaml +54 -0
  31. shrinkwrap/config/cca-3world.yaml +215 -0
  32. shrinkwrap/config/cca-4world.yaml +57 -0
  33. shrinkwrap/config/cca-edk2.yaml +58 -0
  34. shrinkwrap/config/debug/rmm.yaml +15 -0
  35. shrinkwrap/config/debug/tfa.yaml +18 -0
  36. shrinkwrap/config/debug/tftf.yaml +17 -0
  37. shrinkwrap/config/dt-base.yaml +115 -0
  38. shrinkwrap/config/edk2-base.yaml +59 -0
  39. shrinkwrap/config/ffa-hafnium-optee.yaml +45 -0
  40. shrinkwrap/config/ffa-optee.yaml +30 -0
  41. shrinkwrap/config/ffa-tftf.yaml +26 -0
  42. shrinkwrap/config/hafnium-base.yaml +51 -0
  43. shrinkwrap/config/kvm-unit-tests.yaml +32 -0
  44. shrinkwrap/config/kvmtool-base.yaml +33 -0
  45. shrinkwrap/config/linux-base.yaml +80 -0
  46. shrinkwrap/config/ns-edk2-base.yaml +83 -0
  47. shrinkwrap/config/ns-edk2-optee.yaml +41 -0
  48. shrinkwrap/config/ns-edk2.yaml +49 -0
  49. shrinkwrap/config/ns-preload.yaml +98 -0
  50. shrinkwrap/config/optee-base.yaml +37 -0
  51. shrinkwrap/config/rfa-base.yaml +49 -0
  52. shrinkwrap/config/rfa.yaml +47 -0
  53. shrinkwrap/config/rmm-base.yaml +24 -0
  54. shrinkwrap/config/rust.yaml +31 -0
  55. shrinkwrap/config/test/cca.yaml +47 -0
  56. shrinkwrap/config/tfa-base.yaml +45 -0
  57. shrinkwrap/config/tfa-rme.yaml +36 -0
  58. shrinkwrap/config/tftf-base.yaml +32 -0
  59. shrinkwrap/shrinkwrap_main.py +133 -0
  60. shrinkwrap/utils/__init__.py +0 -0
  61. shrinkwrap/utils/clivars.py +16 -0
  62. shrinkwrap/utils/config.py +1166 -0
  63. shrinkwrap/utils/graph.py +263 -0
  64. shrinkwrap/utils/label.py +153 -0
  65. shrinkwrap/utils/logger.py +160 -0
  66. shrinkwrap/utils/process.py +230 -0
  67. shrinkwrap/utils/runtime.py +192 -0
  68. shrinkwrap/utils/ssh_agent.py +98 -0
  69. shrinkwrap/utils/tty.py +46 -0
  70. shrinkwrap/utils/vars.py +14 -0
  71. shrinkwrap/utils/workspace.py +59 -0
  72. shrinkwrap_tool-2026.2.1.dist-info/METADATA +63 -0
  73. shrinkwrap_tool-2026.2.1.dist-info/RECORD +77 -0
  74. shrinkwrap_tool-2026.2.1.dist-info/WHEEL +5 -0
  75. shrinkwrap_tool-2026.2.1.dist-info/entry_points.txt +2 -0
  76. shrinkwrap_tool-2026.2.1.dist-info/licenses/license.rst +41 -0
  77. 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)