opencos-eda 0.2.48__py3-none-any.whl → 0.2.49__py3-none-any.whl

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