Mercurial > hgrepos > Python2 > PyMuPDF
comparison mupdf-source/scripts/jlib.py @ 2:b50eed0cc0ef upstream
ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4.
The directory name has changed: no version number in the expanded directory now.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 11:43:07 +0200 |
| parents | |
| children | 5ab937c03c27 aa33339d6b8a |
comparison
equal
deleted
inserted
replaced
| 1:1d09e1dec1d9 | 2:b50eed0cc0ef |
|---|---|
| 1 import calendar | |
| 2 import codecs | |
| 3 import inspect | |
| 4 import io | |
| 5 import os | |
| 6 import platform | |
| 7 import re | |
| 8 import shlex | |
| 9 import shutil | |
| 10 import subprocess | |
| 11 import sys | |
| 12 import tarfile | |
| 13 import textwrap | |
| 14 import time | |
| 15 import traceback | |
| 16 import types | |
| 17 import typing | |
| 18 | |
| 19 | |
| 20 def place( frame_record=1): | |
| 21 ''' | |
| 22 Useful debugging function - returns representation of source position of | |
| 23 caller. | |
| 24 | |
| 25 frame_record: | |
| 26 Integer number of frames up stack, or a `FrameInfo` (for example from | |
| 27 `inspect.stack()`). | |
| 28 ''' | |
| 29 if isinstance( frame_record, int): | |
| 30 frame_record = inspect.stack( context=0)[ frame_record+1] | |
| 31 filename = frame_record.filename | |
| 32 line = frame_record.lineno | |
| 33 function = frame_record.function | |
| 34 ret = os.path.split( filename)[1] + ':' + str( line) + ':' + function + ':' | |
| 35 if 0: # lgtm [py/unreachable-statement] | |
| 36 tid = str( threading.currentThread()) | |
| 37 ret = '[' + tid + '] ' + ret | |
| 38 return ret | |
| 39 | |
| 40 | |
| 41 def text_nv( text, caller=1): | |
| 42 ''' | |
| 43 Returns `text` with special handling of `{<expression>}` items | |
| 44 constituting an enhanced and deferred form of Python f-strings | |
| 45 (https://docs.python.org/3/reference/lexical_analysis.html#f-strings). | |
| 46 | |
| 47 text: | |
| 48 String containing `{<expression>}` items. | |
| 49 caller: | |
| 50 If an `int`, the number of frames to step up when looking for file:line | |
| 51 information or evaluating expressions. | |
| 52 | |
| 53 Otherwise should be a frame record as returned by `inspect.stack()[]`. | |
| 54 | |
| 55 `<expression>` items are evaluated in `caller`'s context using `eval()`. | |
| 56 | |
| 57 If `expression` ends with `=` or has a `=` before `!` or `:`, this | |
| 58 character is removed and we prefix the result with `<expression>`=. | |
| 59 | |
| 60 >>> x = 45 | |
| 61 >>> y = 'hello' | |
| 62 >>> text_nv( 'foo {x} {y=}') | |
| 63 "foo 45 y='hello'" | |
| 64 | |
| 65 `<expression>` can also use ':' and '!' to control formatting, like | |
| 66 `str.format()`. We support '=' being before (PEP 501) or after the ':' or | |
| 67 `'!'. | |
| 68 | |
| 69 >>> x = 45 | |
| 70 >>> y = 'hello' | |
| 71 >>> text_nv( 'foo {x} {y} {y!r=}') | |
| 72 "foo 45 hello y='hello'" | |
| 73 >>> text_nv( 'foo {x} {y=!r}') | |
| 74 "foo 45 y='hello'" | |
| 75 | |
| 76 If `<expression>` starts with '=', this character is removed and we show | |
| 77 each space-separated item in the remaining text as though it was appended | |
| 78 with '='. | |
| 79 | |
| 80 >>> foo = 45 | |
| 81 >>> y = 'hello' | |
| 82 >>> text_nv('{=foo y}') | |
| 83 "foo=45 y='hello'" | |
| 84 | |
| 85 Also see https://peps.python.org/pep-0501/. | |
| 86 | |
| 87 Check handling of ':' within brackets: | |
| 88 | |
| 89 >>> text_nv('{time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(1670059297))=}') | |
| 90 'time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(1670059297))=\\'2022-12-03 09:21:37\\'' | |
| 91 ''' | |
| 92 if isinstance( caller, int): | |
| 93 frame_record = inspect.stack()[ caller] | |
| 94 else: | |
| 95 frame_record = caller | |
| 96 frame = frame_record.frame | |
| 97 try: | |
| 98 def get_items(): | |
| 99 ''' | |
| 100 Yields `(pre, item)`, where `item` is contents of next `{...}` or | |
| 101 `None`, and `pre` is preceding text. | |
| 102 ''' | |
| 103 pos = 0 | |
| 104 pre = '' | |
| 105 while 1: | |
| 106 if pos == len( text): | |
| 107 yield pre, None | |
| 108 break | |
| 109 rest = text[ pos:] | |
| 110 if rest.startswith( '{{') or rest.startswith( '}}'): | |
| 111 pre += rest[0] | |
| 112 pos += 2 | |
| 113 elif text[ pos] == '{': | |
| 114 close = text.find( '}', pos) | |
| 115 if close < 0: | |
| 116 raise Exception( 'After "{" at offset %s, cannot find closing "}". text is: %r' % ( | |
| 117 pos, text)) | |
| 118 text2 = text[ pos+1 : close] | |
| 119 if text2.startswith('='): | |
| 120 text2 = text2[1:] | |
| 121 for i, text3 in enumerate(text2.split()): | |
| 122 pre2 = ' ' if i else pre | |
| 123 yield pre2, text3 + '=' | |
| 124 else: | |
| 125 yield pre, text[ pos+1 : close] | |
| 126 pre = '' | |
| 127 pos = close + 1 | |
| 128 else: | |
| 129 pre += text[ pos] | |
| 130 pos += 1 | |
| 131 | |
| 132 ret = '' | |
| 133 for pre, item in get_items(): | |
| 134 ret += pre | |
| 135 nv = False | |
| 136 if item: | |
| 137 if item.endswith( '='): | |
| 138 nv = True | |
| 139 item = item[:-1] | |
| 140 expression, tail = text_split_last_of( item, ')]!:') | |
| 141 if tail.startswith( (')', ']')): | |
| 142 expression, tail = item, '' | |
| 143 if expression.endswith('='): | |
| 144 # Basic PEP 501 support. | |
| 145 nv = True | |
| 146 expression = expression[:-1] | |
| 147 if nv and not tail: | |
| 148 # Default to !r as in PEP 501. | |
| 149 tail = '!r' | |
| 150 try: | |
| 151 value = eval( expression, frame.f_globals, frame.f_locals) | |
| 152 value_text = ('{0%s}' % tail).format( value) | |
| 153 except Exception as e: | |
| 154 value_text = '{??Failed to evaluate %r in context %s:%s; expression=%r tail=%r: %s}' % ( | |
| 155 expression, | |
| 156 frame_record.filename, | |
| 157 frame_record.lineno, | |
| 158 expression, | |
| 159 tail, | |
| 160 e, | |
| 161 ) | |
| 162 if nv: | |
| 163 ret += '%s=' % expression | |
| 164 ret += value_text | |
| 165 | |
| 166 return ret | |
| 167 | |
| 168 finally: | |
| 169 del frame # lgtm [py/unnecessary-delete] | |
| 170 | |
| 171 | |
| 172 class LogPrefixTime: | |
| 173 def __init__( self, date=False, time_=True, elapsed=False): | |
| 174 self.date = date | |
| 175 self.time = time_ | |
| 176 self.elapsed = elapsed | |
| 177 self.t0 = time.time() | |
| 178 def __call__( self): | |
| 179 ret = '' | |
| 180 if self.date: | |
| 181 ret += time.strftime( ' %F') | |
| 182 if self.time: | |
| 183 ret += time.strftime( ' %T') | |
| 184 if self.elapsed: | |
| 185 ret += ' (+%s)' % time_duration( time.time() - self.t0, s_format='%.1f') | |
| 186 if ret: | |
| 187 ret = ret.strip() + ': ' | |
| 188 return ret | |
| 189 | |
| 190 class LogPrefixFileLine: | |
| 191 def __call__( self, caller): | |
| 192 if isinstance( caller, int): | |
| 193 caller = inspect.stack()[ caller] | |
| 194 return place( caller) + ' ' | |
| 195 | |
| 196 class LogPrefixScopes: | |
| 197 ''' | |
| 198 Internal use only. | |
| 199 ''' | |
| 200 def __init__( self): | |
| 201 self.items = [] | |
| 202 def __call__( self): | |
| 203 ret = '' | |
| 204 for item in self.items: | |
| 205 if callable( item): | |
| 206 item = item() | |
| 207 ret += item | |
| 208 return ret | |
| 209 | |
| 210 | |
| 211 class LogPrefixScope: | |
| 212 ''' | |
| 213 Can be used to insert scoped prefix to log output. | |
| 214 ''' | |
| 215 def __init__( self, prefix): | |
| 216 self.prefix = prefix | |
| 217 def __enter__( self): | |
| 218 g_log_prefix_scopes.items.append( self.prefix) | |
| 219 def __exit__( self, exc_type, exc_value, traceback): | |
| 220 global g_log_prefix | |
| 221 g_log_prefix_scopes.items.pop() | |
| 222 | |
| 223 | |
| 224 g_log_delta = 0 | |
| 225 | |
| 226 class LogDeltaScope: | |
| 227 ''' | |
| 228 Can be used to temporarily change verbose level of logging. | |
| 229 | |
| 230 E.g to temporarily increase logging:: | |
| 231 | |
| 232 with jlib.LogDeltaScope(-1): | |
| 233 ... | |
| 234 ''' | |
| 235 def __init__( self, delta): | |
| 236 self.delta = delta | |
| 237 global g_log_delta | |
| 238 g_log_delta += self.delta | |
| 239 def __enter__( self): | |
| 240 pass | |
| 241 def __exit__( self, exc_type, exc_value, traceback): | |
| 242 global g_log_delta | |
| 243 g_log_delta -= self.delta | |
| 244 | |
| 245 # Special item that can be inserted into <g_log_prefixes> to enable | |
| 246 # temporary addition of text into log prefixes. | |
| 247 # | |
| 248 g_log_prefix_scopes = LogPrefixScopes() | |
| 249 | |
| 250 # List of items that form prefix for all output from log(). | |
| 251 # | |
| 252 g_log_prefixes = [ | |
| 253 LogPrefixTime( time_=False, elapsed=True), | |
| 254 g_log_prefix_scopes, | |
| 255 LogPrefixFileLine(), | |
| 256 ] | |
| 257 | |
| 258 | |
| 259 _log_text_line_start = True | |
| 260 | |
| 261 def log_text( text=None, caller=1, nv=True, raw=False, nl=True): | |
| 262 ''' | |
| 263 Returns log text, prepending all lines with text from `g_log_prefixes`. | |
| 264 | |
| 265 text: | |
| 266 The text to output. | |
| 267 caller: | |
| 268 If an int, the number of frames to step up when looking for file:line | |
| 269 information or evaluating expressions. | |
| 270 | |
| 271 Otherwise should be a frame record as returned by `inspect.stack()[]`. | |
| 272 nv: | |
| 273 If true, we expand `{...}` in `text` using `jlib.text_nv()`. | |
| 274 raw: | |
| 275 If true we don't terminate with newlines and store state in | |
| 276 `_log_text_line_start` so that we generate correct content if sent sent | |
| 277 partial lines. | |
| 278 nl: | |
| 279 If true (the default) we terminate text with a newline if not already | |
| 280 present. Ignored if `raw` is true. | |
| 281 ''' | |
| 282 if isinstance( caller, int): | |
| 283 caller += 1 | |
| 284 # Construct line prefix. | |
| 285 prefix = '' | |
| 286 for p in g_log_prefixes: | |
| 287 if callable( p): | |
| 288 if isinstance( p, LogPrefixFileLine): | |
| 289 p = p(caller) | |
| 290 else: | |
| 291 p = p() | |
| 292 prefix += p | |
| 293 | |
| 294 if text is None: | |
| 295 return prefix | |
| 296 | |
| 297 # Expand {...} using our enhanced f-string support. | |
| 298 if nv: | |
| 299 text = text_nv( text, caller) | |
| 300 | |
| 301 # Prefix each line. If <raw> is false, we terminate the last line with a | |
| 302 # newline. Otherwise we use _log_text_line_start to remember whether we are | |
| 303 # at the beginning of a line. | |
| 304 # | |
| 305 global _log_text_line_start | |
| 306 text2 = '' | |
| 307 pos = 0 | |
| 308 while 1: | |
| 309 if pos == len(text): | |
| 310 break | |
| 311 if not raw or _log_text_line_start: | |
| 312 text2 += prefix | |
| 313 nlp = text.find('\n', pos) | |
| 314 if nlp == -1: | |
| 315 text2 += text[pos:] | |
| 316 if not raw and nl: | |
| 317 text2 += '\n' | |
| 318 pos = len(text) | |
| 319 else: | |
| 320 text2 += text[pos:nlp+1] | |
| 321 pos = nlp+1 | |
| 322 if raw: | |
| 323 _log_text_line_start = (nlp >= 0) | |
| 324 return text2 | |
| 325 | |
| 326 | |
| 327 s_log_levels_cache = dict() | |
| 328 s_log_levels_items = [] | |
| 329 | |
| 330 def log_levels_find( caller): | |
| 331 if not s_log_levels_items: | |
| 332 return 0 | |
| 333 | |
| 334 tb = traceback.extract_stack( None, 1+caller) | |
| 335 if len(tb) == 0: | |
| 336 return 0 | |
| 337 filename, line, function, text = tb[0] | |
| 338 | |
| 339 key = function, filename, line, | |
| 340 delta = s_log_levels_cache.get( key) | |
| 341 | |
| 342 if delta is None: | |
| 343 # Calculate and populate cache. | |
| 344 delta = 0 | |
| 345 for item_function, item_filename, item_delta in s_log_levels_items: | |
| 346 if item_function and not function.startswith( item_function): | |
| 347 continue | |
| 348 if item_filename and not filename.startswith( item_filename): | |
| 349 continue | |
| 350 delta = item_delta | |
| 351 break | |
| 352 | |
| 353 s_log_levels_cache[ key] = delta | |
| 354 | |
| 355 return delta | |
| 356 | |
| 357 | |
| 358 def log_levels_add( delta, filename_prefix, function_prefix): | |
| 359 ''' | |
| 360 `jlib.log()` calls from locations with filenames starting with | |
| 361 `filename_prefix` and/or function names starting with `function_prefix` | |
| 362 will have `delta` added to their level. | |
| 363 | |
| 364 Use -ve `delta` to increase verbosity from particular filename or function | |
| 365 prefixes. | |
| 366 ''' | |
| 367 log( 'adding level: {filename_prefix=!r} {function_prefix=!r}') | |
| 368 | |
| 369 # Sort in reverse order so that long functions and filename specs come | |
| 370 # first. | |
| 371 # | |
| 372 s_log_levels_items.append( (function_prefix, filename_prefix, delta)) | |
| 373 s_log_levels_items.sort( reverse=True) | |
| 374 | |
| 375 | |
| 376 s_log_out = sys.stdout | |
| 377 | |
| 378 def log( text, level=0, caller=1, nv=True, out=None, raw=False): | |
| 379 ''' | |
| 380 Writes log text, with special handling of `{<expression>}` items in `text` | |
| 381 similar to python3's f-strings. | |
| 382 | |
| 383 text: | |
| 384 The text to output. | |
| 385 level: | |
| 386 Lower values are more verbose. | |
| 387 caller: | |
| 388 How many frames to step up to get caller's context when evaluating | |
| 389 file:line information and/or expressions. Or frame record as returned | |
| 390 by `inspect.stack()[]`. | |
| 391 nv: | |
| 392 If true, we expand `{...}` in `text` using `jlib.text_nv()`. | |
| 393 out: | |
| 394 Where to send output. If None we use sys.stdout. | |
| 395 raw: | |
| 396 If true we don't ensure output text is terminated with a newline. E.g. | |
| 397 use by `jlib.system()` when sending us raw output which is not | |
| 398 line-based. | |
| 399 | |
| 400 `<expression>` is evaluated in our caller's context (`n` stack frames up) | |
| 401 using `eval()`, and expanded to `<expression>` or `<expression>=<value>`. | |
| 402 | |
| 403 If `<expression>` ends with '=', this character is removed and we prefix | |
| 404 the result with <expression>=. | |
| 405 | |
| 406 E.g.:: | |
| 407 | |
| 408 x = 45 | |
| 409 y = 'hello' | |
| 410 text_nv( 'foo {x} {y=}') | |
| 411 | |
| 412 returns:: | |
| 413 | |
| 414 foo 45 y=hello | |
| 415 | |
| 416 `<expression>` can also use ':' and '!' to control formatting, like | |
| 417 `str.format()`. | |
| 418 ''' | |
| 419 if out is None: | |
| 420 out = s_log_out | |
| 421 level += g_log_delta | |
| 422 if isinstance( caller, int): | |
| 423 caller += 1 | |
| 424 level += log_levels_find( caller) | |
| 425 if level <= 0: | |
| 426 text = log_text( text, caller, nv=nv, raw=raw) | |
| 427 try: | |
| 428 out.write( text) | |
| 429 except UnicodeEncodeError: | |
| 430 # Retry, ignoring errors by encoding then decoding with | |
| 431 # errors='replace'. | |
| 432 # | |
| 433 out.write('[***write encoding error***]') | |
| 434 text_encoded = codecs.encode(text, out.encoding, errors='replace') | |
| 435 text_encoded_decoded = codecs.decode(text_encoded, out.encoding, errors='replace') | |
| 436 out.write(text_encoded_decoded) | |
| 437 out.write('[/***write encoding error***]') | |
| 438 out.flush() | |
| 439 | |
| 440 def log_raw( text, level=0, caller=1, nv=False, out=None): | |
| 441 ''' | |
| 442 Like `jlib.log()` but defaults to `nv=False` so any `{...}` are not | |
| 443 evaluated as expressions. | |
| 444 | |
| 445 Useful for things like:: | |
| 446 | |
| 447 jlib.system(..., out=jlib.log_raw) | |
| 448 ''' | |
| 449 log( text, level=0, caller=caller+1, nv=nv, out=out) | |
| 450 | |
| 451 def log0( text, caller=1, nv=True, out=None): | |
| 452 ''' | |
| 453 Most verbose log. Same as log(). | |
| 454 ''' | |
| 455 log( text, level=0, caller=caller+1, nv=nv, out=out) | |
| 456 | |
| 457 def log1( text, caller=1, nv=True, out=None): | |
| 458 log( text, level=1, caller=caller+1, nv=nv, out=out) | |
| 459 | |
| 460 def log2( text, caller=1, nv=True, out=None): | |
| 461 log( text, level=2, caller=caller+1, nv=nv, out=out) | |
| 462 | |
| 463 def log3( text, caller=1, nv=True, out=None): | |
| 464 log( text, level=3, caller=caller+1, nv=nv, out=out) | |
| 465 | |
| 466 def log4( text, caller=1, nv=True, out=None): | |
| 467 log( text, level=4, caller=caller+1, nv=nv, out=out) | |
| 468 | |
| 469 def log5( text, caller=1, nv=True, out=None): | |
| 470 ''' | |
| 471 Least verbose log. | |
| 472 ''' | |
| 473 log( text, level=5, caller=caller+1, nv=nv, out=out) | |
| 474 | |
| 475 def logx( text, caller=1, nv=True, out=None): | |
| 476 ''' | |
| 477 Does nothing, useful when commenting out a log(). | |
| 478 ''' | |
| 479 pass | |
| 480 | |
| 481 | |
| 482 _log_interval_t0 = 0 | |
| 483 | |
| 484 def log_interval( text, level=0, caller=1, nv=True, out=None, raw=False, interval=10): | |
| 485 ''' | |
| 486 Like `jlib.log()` but outputs no more than one diagnostic every `interval` | |
| 487 seconds, and `text` can be a callable taking no args and returning a | |
| 488 string. | |
| 489 ''' | |
| 490 global _log_interval_t0 | |
| 491 t = time.time() | |
| 492 if t - _log_interval_t0 > interval: | |
| 493 _log_interval_t0 = t | |
| 494 if callable( text): | |
| 495 text = text() | |
| 496 log( text, level=level, caller=caller+1, nv=nv, out=out, raw=raw) | |
| 497 | |
| 498 | |
| 499 def log_levels_add_env( name='JLIB_log_levels'): | |
| 500 ''' | |
| 501 Added log levels encoded in an environmental variable. | |
| 502 ''' | |
| 503 t = os.environ.get( name) | |
| 504 if t: | |
| 505 for ffll in t.split( ','): | |
| 506 ffl, delta = ffll.split( '=', 1) | |
| 507 delta = int( delta) | |
| 508 ffl = ffl.split( ':') | |
| 509 if 0: # lgtm [py/unreachable-statement] | |
| 510 pass | |
| 511 elif len( ffl) == 1: | |
| 512 filename = ffl | |
| 513 function = None | |
| 514 elif len( ffl) == 2: | |
| 515 filename, function = ffl | |
| 516 else: | |
| 517 assert 0 | |
| 518 log_levels_add( delta, filename, function) | |
| 519 | |
| 520 | |
| 521 class TimingsItem: | |
| 522 ''' | |
| 523 Helper for `Timings` class. | |
| 524 ''' | |
| 525 def __init__( self, name): | |
| 526 self.name = name | |
| 527 self.children = dict() | |
| 528 self.t_begin = None | |
| 529 self.t = 0 | |
| 530 self.n = 0 | |
| 531 def begin( self, t): | |
| 532 assert self.t_begin is None | |
| 533 self.t_begin = t | |
| 534 def end( self, t): | |
| 535 assert self.t_begin is not None, f't_begin is None, .name={self.name}' | |
| 536 self.t += t - self.t_begin | |
| 537 self.n += 1 | |
| 538 self.t_begin = None | |
| 539 def __str__( self): | |
| 540 return f'[name={self.name} t={self.t} n={self.n} t_begin={self.t_begin}]' | |
| 541 def __repr__( self): | |
| 542 return self.__str__() | |
| 543 | |
| 544 class Timings: | |
| 545 ''' | |
| 546 Allows gathering of hierarchical timing information. Can also generate | |
| 547 useful diagnostics. | |
| 548 | |
| 549 Caller can generate a tree of `TimingsItem` items via our `begin()` and | |
| 550 `end()` methods. | |
| 551 | |
| 552 >>> ts = Timings() | |
| 553 >>> ts.begin('a') | |
| 554 >>> time.sleep(0.1) | |
| 555 >>> ts.begin('b') | |
| 556 >>> time.sleep(0.2) | |
| 557 >>> ts.begin('c') | |
| 558 >>> time.sleep(0.3) | |
| 559 >>> ts.end('c') | |
| 560 >>> ts.begin('c') | |
| 561 >>> time.sleep(0.3) | |
| 562 >>> ts.end('b') # will also end 'c'. | |
| 563 >>> ts.begin('d') | |
| 564 >>> ts.begin('e') | |
| 565 >>> time.sleep(0.1) | |
| 566 >>> ts.end_all() # will end everything. | |
| 567 >>> print(ts) | |
| 568 Timings (in seconds): | |
| 569 1.0 a | |
| 570 0.8 b | |
| 571 0.6/2 c | |
| 572 0.1 d | |
| 573 0.1 e | |
| 574 <BLANKLINE> | |
| 575 | |
| 576 One can also use as a context manager: | |
| 577 | |
| 578 >>> ts = Timings() | |
| 579 >>> with ts( 'foo'): | |
| 580 ... time.sleep(1) | |
| 581 ... with ts( 'bar'): | |
| 582 ... time.sleep(1) | |
| 583 >>> print( ts) | |
| 584 Timings (in seconds): | |
| 585 2.0 foo | |
| 586 1.0 bar | |
| 587 <BLANKLINE> | |
| 588 | |
| 589 Must specify name, otherwise we assert-fail. | |
| 590 | |
| 591 >>> with ts: | |
| 592 ... pass | |
| 593 Traceback (most recent call last): | |
| 594 AssertionError: Must specify <name> etc when using "with ...". | |
| 595 ''' | |
| 596 def __init__( self, name='', active=True): | |
| 597 ''' | |
| 598 If `active` is False, returned instance does nothing. | |
| 599 ''' | |
| 600 self.active = active | |
| 601 self.root_item = TimingsItem( name) | |
| 602 self.nest = [ self.root_item] | |
| 603 self.nest[0].begin( time.time()) | |
| 604 self.name_max_len = 0 | |
| 605 self.call_enter_state = None | |
| 606 self.call_enter_stack = [] | |
| 607 | |
| 608 def begin( self, name=None, text=None, level=0, t=None): | |
| 609 ''' | |
| 610 Starts a new timing item as child of most recent in-progress timing | |
| 611 item. | |
| 612 | |
| 613 name: | |
| 614 Used in final statistics. If `None`, we use `jlib.place()`. | |
| 615 text: | |
| 616 If not `None`, this is output here with `jlib.log()`. | |
| 617 level: | |
| 618 Verbosity. Added to `g_verbose`. | |
| 619 ''' | |
| 620 if not self.active: | |
| 621 return | |
| 622 if t is None: | |
| 623 t = time.time() | |
| 624 if name is None: | |
| 625 name = place(2) | |
| 626 self.name_max_len = max( self.name_max_len, len(name)) | |
| 627 leaf = self.nest[-1].children.setdefault( name, TimingsItem( name)) | |
| 628 self.nest.append( leaf) | |
| 629 leaf.begin( t) | |
| 630 if text: | |
| 631 log( text, nv=0) | |
| 632 | |
| 633 def end( self, name=None, t=None): | |
| 634 ''' | |
| 635 Repeatedly ends the most recent item until we have ended item called | |
| 636 `name`. Ends just the most recent item if name is `None`. | |
| 637 ''' | |
| 638 if not self.active: | |
| 639 return | |
| 640 if t is None: | |
| 641 t = time.time() | |
| 642 if name is None: | |
| 643 name = self.nest[-1].name | |
| 644 while self.nest: | |
| 645 leaf = self.nest.pop() | |
| 646 leaf.end( t) | |
| 647 if leaf.name == name: | |
| 648 break | |
| 649 else: | |
| 650 if name is not None: | |
| 651 log( f'*** Warning: cannot end timing item called {name} because not found.') | |
| 652 | |
| 653 def end_all( self): | |
| 654 self.end( self.nest[0].name) | |
| 655 | |
| 656 def mid( self, name=None): | |
| 657 ''' | |
| 658 Ends current leaf item and starts a new item called `name`. Useful to | |
| 659 define multiple timing blocks at same level. | |
| 660 ''' | |
| 661 if not self.active: | |
| 662 return | |
| 663 t = time.time() | |
| 664 if len( self.nest) > 1: | |
| 665 self.end( self.nest[-1].name, t) | |
| 666 self.begin( name, t=t) | |
| 667 | |
| 668 def __enter__( self): | |
| 669 if not self.active: | |
| 670 return | |
| 671 assert self.call_enter_state, 'Must specify <name> etc when using "with ...".' | |
| 672 name, text, level = self.call_enter_state | |
| 673 self.begin( name, text, level) | |
| 674 self.call_enter_state = None | |
| 675 self.call_enter_stack.append( name) | |
| 676 | |
| 677 def __exit__( self, type, value, traceback): | |
| 678 if not self.active: | |
| 679 return | |
| 680 assert not self.call_enter_state, f'self.call_enter_state is not false: {self.call_enter_state}' | |
| 681 name = self.call_enter_stack.pop() | |
| 682 self.end( name) | |
| 683 | |
| 684 def __call__( self, name=None, text=None, level=0): | |
| 685 ''' | |
| 686 Allow scoped timing. | |
| 687 ''' | |
| 688 if not self.active: | |
| 689 return self | |
| 690 assert not self.call_enter_state, f'self.call_enter_state is not false: {self.call_enter_state}' | |
| 691 self.call_enter_state = ( name, text, level) | |
| 692 return self | |
| 693 | |
| 694 def text( self, item, depth=0, precision=1): | |
| 695 ''' | |
| 696 Returns text showing hierarchical timing information. | |
| 697 ''' | |
| 698 if not self.active: | |
| 699 return '' | |
| 700 if item is self.root_item and not item.name: | |
| 701 # Don't show top-level. | |
| 702 ret = '' | |
| 703 else: | |
| 704 tt = ' None' if item.t is None else f'{item.t:6.{precision}f}' | |
| 705 n = f'/{item.n}' if item.n >= 2 else '' | |
| 706 ret = f'{" " * 4 * depth} {tt}{n} {item.name}\n' | |
| 707 depth += 1 | |
| 708 for _, timing2 in item.children.items(): | |
| 709 ret += self.text( timing2, depth, precision) | |
| 710 return ret | |
| 711 | |
| 712 def __str__( self): | |
| 713 ret = 'Timings (in seconds):\n' | |
| 714 ret += self.text( self.root_item, 0) | |
| 715 return ret | |
| 716 | |
| 717 | |
| 718 def text_strpbrk_reverse( text, substrings): | |
| 719 ''' | |
| 720 Finds last occurrence of any item in `substrings` in `text`. | |
| 721 | |
| 722 Returns `(pos, substring)` or `(len(text), None)` if not found. | |
| 723 ''' | |
| 724 ret_pos = -1 | |
| 725 ret_substring = None | |
| 726 for substring in substrings: | |
| 727 pos = text.rfind( substring) | |
| 728 if pos >= 0 and pos > ret_pos: | |
| 729 ret_pos = pos | |
| 730 ret_substring = substring | |
| 731 if ret_pos == -1: | |
| 732 ret_pos = len( text) | |
| 733 return ret_pos, ret_substring | |
| 734 | |
| 735 | |
| 736 def text_split_last_of( text, substrings): | |
| 737 ''' | |
| 738 Returns `(pre, post)`, where `pre` doesn't contain any item in `substrings` | |
| 739 and `post` is empty or starts with an item in `substrings`. | |
| 740 ''' | |
| 741 pos, _ = text_strpbrk_reverse( text, substrings) | |
| 742 | |
| 743 return text[ :pos], text[ pos:] | |
| 744 | |
| 745 | |
| 746 | |
| 747 log_levels_add_env() | |
| 748 | |
| 749 | |
| 750 def force_line_buffering(): | |
| 751 ''' | |
| 752 Ensure `sys.stdout` and `sys.stderr` are line-buffered. E.g. makes things | |
| 753 work better if output is piped to a file via 'tee'. | |
| 754 | |
| 755 Returns original out,err streams. | |
| 756 ''' | |
| 757 stdout0 = sys.stdout | |
| 758 stderr0 = sys.stderr | |
| 759 sys.stdout = os.fdopen( sys.stdout.fileno(), 'w', 1) | |
| 760 sys.stderr = os.fdopen( sys.stderr.fileno(), 'w', 1) | |
| 761 return stdout0, stderr0 | |
| 762 | |
| 763 | |
| 764 def exception_info( | |
| 765 exception_or_traceback=None, | |
| 766 limit=None, | |
| 767 file=None, | |
| 768 chain=True, | |
| 769 outer=True, | |
| 770 show_exception_type=True, | |
| 771 _filelinefn=True, | |
| 772 ): | |
| 773 ''' | |
| 774 Shows an exception and/or backtrace. | |
| 775 | |
| 776 Alternative to `traceback.*` functions that print/return information about | |
| 777 exceptions and backtraces, such as: | |
| 778 | |
| 779 * `traceback.format_exc()` | |
| 780 * `traceback.format_exception()` | |
| 781 * `traceback.print_exc()` | |
| 782 * `traceback.print_exception()` | |
| 783 | |
| 784 Install as system default with: | |
| 785 | |
| 786 `sys.excepthook = lambda type_, exception, traceback: jlib.exception_info( exception)` | |
| 787 | |
| 788 Returns `None`, or the generated text if `file` is 'return'. | |
| 789 | |
| 790 Args: | |
| 791 exception_or_traceback: | |
| 792 `None`, a `BaseException`, a `types.TracebackType` (typically from | |
| 793 an exception's `.__traceback__` member) or an `inspect.FrameInfo`. | |
| 794 | |
| 795 If `None` we use current exception from `sys.exc_info()` if set, | |
| 796 otherwise the current backtrace from `inspect.stack()`. | |
| 797 limit: | |
| 798 As in `traceback.*` functions: `None` to show all frames, positive | |
| 799 to show last `limit` frames, negative to exclude outermost `-limit` | |
| 800 frames. Zero to not show any backtraces. | |
| 801 file: | |
| 802 As in `traceback.*` functions: file-like object to which we write | |
| 803 output, or `sys.stderr` if `None`. Special value 'return' makes us | |
| 804 return our output as a string. | |
| 805 chain: | |
| 806 As in `traceback.*` functions: if true (the default) we show | |
| 807 chained exceptions as described in PEP-3134. Special value | |
| 808 'because' reverses the usual ordering, showing higher-level | |
| 809 exceptions first and joining with 'Because:' text. | |
| 810 outer: | |
| 811 If true (the default) we also show an exception's outer frames | |
| 812 above the `catch` block (see next section for details). We | |
| 813 use `outer=false` internally for chained exceptions to avoid | |
| 814 duplication. | |
| 815 show_exception_type: | |
| 816 Controls whether exception text is prefixed by | |
| 817 `f'{type(exception)}: '`. If callable we only include this prefix | |
| 818 if `show_exception_type(exception)` is true. Otherwise if true (the | |
| 819 default) we include the prefix for all exceptions (this mimcs the | |
| 820 behaviour of `traceback.*` functions). Otherwise we exclude the | |
| 821 prefix for all exceptions. | |
| 822 _filelinefn: | |
| 823 Internal only; makes us omit file:line: information to allow simple | |
| 824 doctest comparison with expected output. | |
| 825 | |
| 826 Differences from `traceback.*` functions: | |
| 827 | |
| 828 Frames are displayed as one line in the form:: | |
| 829 | |
| 830 <file>:<line>:<function>: <text> | |
| 831 | |
| 832 Filenames are displayed as relative to the current directory if | |
| 833 applicable. | |
| 834 | |
| 835 Inclusion of outer frames: | |
| 836 Unlike `traceback.*` functions, stack traces for exceptions include | |
| 837 outer stack frames above the point at which an exception was caught | |
| 838 - i.e. frames from the top-level <module> or thread creation to the | |
| 839 catch block. [Search for 'sys.exc_info backtrace incomplete' for | |
| 840 more details.] | |
| 841 | |
| 842 We separate the two parts of the backtrace using a marker line | |
| 843 '^except raise:' where '^except' points upwards to the frame that | |
| 844 caught the exception and 'raise:' refers downwards to the frame | |
| 845 that raised the exception. | |
| 846 | |
| 847 So the backtrace for an exception looks like this:: | |
| 848 | |
| 849 <file>:<line>:<fn>: <text> [in root module.] | |
| 850 ... [... other frames] | |
| 851 <file>:<line>:<fn>: <text> [in except: block where exception was caught.] | |
| 852 ^except raise: [marker line] | |
| 853 <file>:<line>:<fn>: <text> [in try: block.] | |
| 854 ... [... other frames] | |
| 855 <file>:<line>:<fn>: <text> [where the exception was raised.] | |
| 856 | |
| 857 Examples: | |
| 858 | |
| 859 In these examples we use `file=sys.stdout` so we can check the output | |
| 860 with `doctest`, and set `_filelinefn=0` so that the output can be | |
| 861 matched easily. We also use `+ELLIPSIS` and `...` to match arbitrary | |
| 862 outer frames from the doctest code itself. | |
| 863 | |
| 864 Basic handling of an exception: | |
| 865 | |
| 866 >>> def c(): | |
| 867 ... raise Exception( 'c() failed') | |
| 868 >>> def b(): | |
| 869 ... try: | |
| 870 ... c() | |
| 871 ... except Exception as e: | |
| 872 ... exception_info( e, file=sys.stdout, _filelinefn=0) | |
| 873 >>> def a(): | |
| 874 ... b() | |
| 875 | |
| 876 >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 877 Traceback (most recent call last): | |
| 878 ... | |
| 879 a(): b() | |
| 880 b(): exception_info( e, file=sys.stdout, _filelinefn=0) | |
| 881 ^except raise: | |
| 882 b(): c() | |
| 883 c(): raise Exception( 'c() failed') | |
| 884 Exception: c() failed | |
| 885 | |
| 886 Handling of chained exceptions: | |
| 887 | |
| 888 >>> def e(): | |
| 889 ... raise Exception( 'e(): deliberate error') | |
| 890 >>> def d(): | |
| 891 ... e() | |
| 892 >>> def c(): | |
| 893 ... try: | |
| 894 ... d() | |
| 895 ... except Exception as e: | |
| 896 ... raise Exception( 'c: d() failed') from e | |
| 897 >>> def b(): | |
| 898 ... try: | |
| 899 ... c() | |
| 900 ... except Exception as e: | |
| 901 ... exception_info( file=sys.stdout, chain=g_chain, _filelinefn=0) | |
| 902 >>> def a(): | |
| 903 ... b() | |
| 904 | |
| 905 With `chain=True` (the default), we output low-level exceptions | |
| 906 first, matching the behaviour of `traceback.*` functions: | |
| 907 | |
| 908 >>> g_chain = True | |
| 909 >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 910 Traceback (most recent call last): | |
| 911 c(): d() | |
| 912 d(): e() | |
| 913 e(): raise Exception( 'e(): deliberate error') | |
| 914 Exception: e(): deliberate error | |
| 915 <BLANKLINE> | |
| 916 The above exception was the direct cause of the following exception: | |
| 917 Traceback (most recent call last): | |
| 918 ... | |
| 919 <module>(): a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 920 a(): b() | |
| 921 b(): exception_info( file=sys.stdout, chain=g_chain, _filelinefn=0) | |
| 922 ^except raise: | |
| 923 b(): c() | |
| 924 c(): raise Exception( 'c: d() failed') from e | |
| 925 Exception: c: d() failed | |
| 926 | |
| 927 With `chain='because'`, we output high-level exceptions first: | |
| 928 >>> g_chain = 'because' | |
| 929 >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 930 Traceback (most recent call last): | |
| 931 ... | |
| 932 <module>(): a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 933 a(): b() | |
| 934 b(): exception_info( file=sys.stdout, chain=g_chain, _filelinefn=0) | |
| 935 ^except raise: | |
| 936 b(): c() | |
| 937 c(): raise Exception( 'c: d() failed') from e | |
| 938 Exception: c: d() failed | |
| 939 <BLANKLINE> | |
| 940 Because: | |
| 941 Traceback (most recent call last): | |
| 942 c(): d() | |
| 943 d(): e() | |
| 944 e(): raise Exception( 'e(): deliberate error') | |
| 945 Exception: e(): deliberate error | |
| 946 | |
| 947 Show current backtrace by passing `exception_or_traceback=None`: | |
| 948 >>> def c(): | |
| 949 ... exception_info( None, file=sys.stdout, _filelinefn=0) | |
| 950 >>> def b(): | |
| 951 ... return c() | |
| 952 >>> def a(): | |
| 953 ... return b() | |
| 954 | |
| 955 >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 956 Traceback (most recent call last): | |
| 957 ... | |
| 958 <module>(): a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 959 a(): return b() | |
| 960 b(): return c() | |
| 961 c(): exception_info( None, file=sys.stdout, _filelinefn=0) | |
| 962 | |
| 963 Show an exception's `.__traceback__` backtrace: | |
| 964 >>> def c(): | |
| 965 ... raise Exception( 'foo') # raise | |
| 966 >>> def b(): | |
| 967 ... return c() # call c | |
| 968 >>> def a(): | |
| 969 ... try: | |
| 970 ... b() # call b | |
| 971 ... except Exception as e: | |
| 972 ... exception_info( e.__traceback__, file=sys.stdout, _filelinefn=0) | |
| 973 | |
| 974 >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS | |
| 975 Traceback (most recent call last): | |
| 976 ... | |
| 977 a(): b() # call b | |
| 978 b(): return c() # call c | |
| 979 c(): raise Exception( 'foo') # raise | |
| 980 ''' | |
| 981 # Set exactly one of <exception> and <tb>. | |
| 982 # | |
| 983 if isinstance( exception_or_traceback, (types.TracebackType, inspect.FrameInfo)): | |
| 984 # Simple backtrace, no Exception information. | |
| 985 exception = None | |
| 986 tb = exception_or_traceback | |
| 987 elif isinstance( exception_or_traceback, BaseException): | |
| 988 exception = exception_or_traceback | |
| 989 tb = None | |
| 990 elif exception_or_traceback is None: | |
| 991 # Show exception if available, else backtrace. | |
| 992 _, exception, tb = sys.exc_info() | |
| 993 tb = None if exception else inspect.stack()[1:] | |
| 994 else: | |
| 995 assert 0, f'Unrecognised exception_or_traceback type: {type(exception_or_traceback)}' | |
| 996 | |
| 997 if file == 'return': | |
| 998 out = io.StringIO() | |
| 999 else: | |
| 1000 out = file if file else sys.stderr | |
| 1001 | |
| 1002 def do_chain( exception): | |
| 1003 exception_info( | |
| 1004 exception, | |
| 1005 limit, | |
| 1006 out, | |
| 1007 chain, | |
| 1008 outer=False, | |
| 1009 show_exception_type=show_exception_type, | |
| 1010 _filelinefn=_filelinefn, | |
| 1011 ) | |
| 1012 | |
| 1013 if exception and chain and chain != 'because' and chain != 'because-compact': | |
| 1014 # Output current exception first. | |
| 1015 if exception.__cause__: | |
| 1016 do_chain( exception.__cause__) | |
| 1017 out.write( '\nThe above exception was the direct cause of the following exception:\n') | |
| 1018 elif exception.__context__: | |
| 1019 do_chain( exception.__context__) | |
| 1020 out.write( '\nDuring handling of the above exception, another exception occurred:\n') | |
| 1021 | |
| 1022 cwd = os.getcwd() + os.sep | |
| 1023 | |
| 1024 def output_frames( frames, reverse, limit): | |
| 1025 if limit == 0: | |
| 1026 return | |
| 1027 if reverse: | |
| 1028 assert isinstance( frames, list) | |
| 1029 frames = reversed( frames) | |
| 1030 if limit is not None: | |
| 1031 frames = list( frames) | |
| 1032 frames = frames[ -limit:] | |
| 1033 for frame in frames: | |
| 1034 f, filename, line, fnname, text, index = frame | |
| 1035 text = text[0].strip() if text else '' | |
| 1036 if filename.startswith( cwd): | |
| 1037 filename = filename[ len(cwd):] | |
| 1038 if filename.startswith( f'.{os.sep}'): | |
| 1039 filename = filename[ 2:] | |
| 1040 if _filelinefn: | |
| 1041 out.write( f' {filename}:{line}:{fnname}(): {text}\n') | |
| 1042 else: | |
| 1043 out.write( f' {fnname}(): {text}\n') | |
| 1044 | |
| 1045 if limit != 0: | |
| 1046 out.write( 'Traceback (most recent call last):\n') | |
| 1047 if exception: | |
| 1048 tb = exception.__traceback__ | |
| 1049 assert tb | |
| 1050 if outer: | |
| 1051 output_frames( inspect.getouterframes( tb.tb_frame), reverse=True, limit=limit) | |
| 1052 out.write( ' ^except raise:\n') | |
| 1053 limit2 = 0 if limit == 0 else None | |
| 1054 output_frames( inspect.getinnerframes( tb), reverse=False, limit=limit2) | |
| 1055 else: | |
| 1056 if not isinstance( tb, list): | |
| 1057 inner = inspect.getinnerframes(tb) | |
| 1058 outer = inspect.getouterframes(tb.tb_frame) | |
| 1059 tb = outer + inner | |
| 1060 tb.reverse() | |
| 1061 output_frames( tb, reverse=True, limit=limit) | |
| 1062 | |
| 1063 if exception: | |
| 1064 if callable(show_exception_type): | |
| 1065 show_exception_type2 = show_exception_type( exception) | |
| 1066 else: | |
| 1067 show_exception_type2 = show_exception_type | |
| 1068 if show_exception_type2: | |
| 1069 lines = traceback.format_exception_only( type(exception), exception) | |
| 1070 for line in lines: | |
| 1071 out.write( line) | |
| 1072 else: | |
| 1073 out.write( str( exception) + '\n') | |
| 1074 | |
| 1075 if exception and (chain == 'because' or chain == 'because-compact'): | |
| 1076 # Output current exception afterwards. | |
| 1077 pre, post = ('\n', '\n') if chain == 'because' else ('', ' ') | |
| 1078 if exception.__cause__: | |
| 1079 out.write( f'{pre}Because:{post}') | |
| 1080 do_chain( exception.__cause__) | |
| 1081 elif exception.__context__: | |
| 1082 out.write( f'{pre}Because: error occurred handling this exception:{post}') | |
| 1083 do_chain( exception.__context__) | |
| 1084 | |
| 1085 if file == 'return': | |
| 1086 return out.getvalue() | |
| 1087 | |
| 1088 | |
| 1089 def number_sep( s): | |
| 1090 ''' | |
| 1091 Simple number formatter, adds commas in-between thousands. `s` can be a | |
| 1092 number or a string. Returns a string. | |
| 1093 | |
| 1094 >>> number_sep(1) | |
| 1095 '1' | |
| 1096 >>> number_sep(12) | |
| 1097 '12' | |
| 1098 >>> number_sep(123) | |
| 1099 '123' | |
| 1100 >>> number_sep(1234) | |
| 1101 '1,234' | |
| 1102 >>> number_sep(12345) | |
| 1103 '12,345' | |
| 1104 >>> number_sep(123456) | |
| 1105 '123,456' | |
| 1106 >>> number_sep(1234567) | |
| 1107 '1,234,567' | |
| 1108 ''' | |
| 1109 if not isinstance( s, str): | |
| 1110 s = str( s) | |
| 1111 c = s.find( '.') | |
| 1112 if c==-1: c = len(s) | |
| 1113 end = s.find('e') | |
| 1114 if end == -1: end = s.find('E') | |
| 1115 if end == -1: end = len(s) | |
| 1116 ret = '' | |
| 1117 for i in range( end): | |
| 1118 ret += s[i] | |
| 1119 if i<c-1 and (c-i-1)%3==0: | |
| 1120 ret += ',' | |
| 1121 elif i>c and i<end-1 and (i-c)%3==0: | |
| 1122 ret += ',' | |
| 1123 ret += s[end:] | |
| 1124 return ret | |
| 1125 | |
| 1126 | |
| 1127 class Stream: | |
| 1128 ''' | |
| 1129 Base layering abstraction for streams - abstraction for things like | |
| 1130 `sys.stdout` to allow prefixing of all output, e.g. with a timestamp. | |
| 1131 ''' | |
| 1132 def __init__( self, stream): | |
| 1133 self.stream = stream | |
| 1134 def write( self, text): | |
| 1135 self.stream.write( text) | |
| 1136 | |
| 1137 class StreamPrefix: | |
| 1138 ''' | |
| 1139 Prefixes output with a prefix, which can be a string, or a callable that | |
| 1140 takes no parameters and return a string, or an integer number of spaces. | |
| 1141 ''' | |
| 1142 def __init__( self, stream, prefix): | |
| 1143 if callable(stream): | |
| 1144 self.stream_write = stream | |
| 1145 self.stream_flush = lambda: None | |
| 1146 else: | |
| 1147 self.stream_write = stream.write | |
| 1148 self.stream_flush = stream.flush | |
| 1149 self.at_start = True | |
| 1150 if callable(prefix): | |
| 1151 self.prefix = prefix | |
| 1152 elif isinstance( prefix, int): | |
| 1153 self.prefix = lambda: ' ' * prefix | |
| 1154 else: | |
| 1155 self.prefix = lambda : prefix | |
| 1156 | |
| 1157 def write( self, text): | |
| 1158 if self.at_start: | |
| 1159 text = self.prefix() + text | |
| 1160 self.at_start = False | |
| 1161 append_newline = False | |
| 1162 if text.endswith( '\n'): | |
| 1163 text = text[:-1] | |
| 1164 self.at_start = True | |
| 1165 append_newline = True | |
| 1166 text = text.replace( '\n', '\n%s' % self.prefix()) | |
| 1167 if append_newline: | |
| 1168 text += '\n' | |
| 1169 self.stream_write( text) | |
| 1170 | |
| 1171 def flush( self): | |
| 1172 self.stream_flush() | |
| 1173 | |
| 1174 | |
| 1175 def time_duration( seconds, verbose=False, s_format='%i'): | |
| 1176 ''' | |
| 1177 Returns string expressing an interval. | |
| 1178 | |
| 1179 seconds: | |
| 1180 The duration in seconds | |
| 1181 verbose: | |
| 1182 If true, return like '4 days 1 hour 2 mins 23 secs', otherwise as | |
| 1183 '4d3h2m23s'. | |
| 1184 s_format: | |
| 1185 If specified, use as printf-style format string for seconds. | |
| 1186 | |
| 1187 >>> time_duration( 303333) | |
| 1188 '3d12h15m33s' | |
| 1189 | |
| 1190 We pad single-digit numbers with '0' to keep things aligned: | |
| 1191 >>> time_duration( 302703.33, s_format='%.1f') | |
| 1192 '3d12h05m03.3s' | |
| 1193 | |
| 1194 When verbose, we pad single-digit numbers with ' ' to keep things aligned: | |
| 1195 >>> time_duration( 302703, verbose=True) | |
| 1196 '3 days 12 hours 5 mins 3 secs' | |
| 1197 | |
| 1198 >>> time_duration( 302703.33, verbose=True, s_format='%.1f') | |
| 1199 '3 days 12 hours 5 mins 3.3 secs' | |
| 1200 | |
| 1201 >>> time_duration( 0) | |
| 1202 '0s' | |
| 1203 | |
| 1204 >>> time_duration( 0, verbose=True) | |
| 1205 '0 sec' | |
| 1206 ''' | |
| 1207 x = abs(seconds) | |
| 1208 ret = '' | |
| 1209 i = 0 | |
| 1210 for div, text in [ | |
| 1211 ( 60, 'sec'), | |
| 1212 ( 60, 'min'), | |
| 1213 ( 24, 'hour'), | |
| 1214 ( None, 'day'), | |
| 1215 ]: | |
| 1216 force = ( x == 0 and i == 0) | |
| 1217 if div: | |
| 1218 remainder = x % div | |
| 1219 x = int( x/div) | |
| 1220 else: | |
| 1221 remainder = x | |
| 1222 x = 0 | |
| 1223 if not verbose: | |
| 1224 text = text[0] | |
| 1225 if remainder or force: | |
| 1226 if verbose and remainder > 1: | |
| 1227 # plural. | |
| 1228 text += 's' | |
| 1229 if verbose: | |
| 1230 text = ' %s ' % text | |
| 1231 if i == 0: | |
| 1232 remainder_string = s_format % remainder | |
| 1233 else: | |
| 1234 remainder_string = str( remainder) | |
| 1235 if x and (remainder < 10): | |
| 1236 # Pad with space or '0' to keep alignment. | |
| 1237 pad = ' ' if verbose else '0' | |
| 1238 remainder_string = pad + str(remainder_string) | |
| 1239 ret = '%s%s%s' % ( remainder_string, text, ret) | |
| 1240 i += 1 | |
| 1241 ret = ret.strip() | |
| 1242 if ret == '': | |
| 1243 ret = '0s' | |
| 1244 if seconds < 0: | |
| 1245 ret = '-%s' % ret | |
| 1246 return ret | |
| 1247 | |
| 1248 | |
| 1249 def date_time( t=None): | |
| 1250 if t is None: | |
| 1251 t = time.time() | |
| 1252 return time.strftime( "%F-%T", time.gmtime( t)) | |
| 1253 | |
| 1254 | |
| 1255 def time_read_date1( text): | |
| 1256 ''' | |
| 1257 <text> is: | |
| 1258 <year>-<month>-<day>-<hour>-<min>-<sec> | |
| 1259 | |
| 1260 Trailing values can be omitted, e.g. `2004-3' is treated as | |
| 1261 2004-03-0-0-0-0, i.e. 1st of March 2004. I think GMT is used, | |
| 1262 not the local time though. | |
| 1263 | |
| 1264 >>> assert time_read_date1( '2010') == calendar.timegm( ( 2010, 1, 1, 0, 0, 0, 0, 0, 0)) | |
| 1265 >>> assert time_read_date1( '2010-1') == calendar.timegm( ( 2010, 1, 1, 0, 0, 0, 0, 0, 0)) | |
| 1266 >>> assert time_read_date1( '2015-4-25-14-39-39') == calendar.timegm( time.strptime( 'Sat Apr 25 14:39:39 2015')) | |
| 1267 ''' | |
| 1268 pieces = text.split( '-') | |
| 1269 if len( pieces) == 1: | |
| 1270 pieces.append( '1') # mon | |
| 1271 if len( pieces) == 2: | |
| 1272 pieces.append( '1') # mday | |
| 1273 if len( pieces) == 3: | |
| 1274 pieces.append( '0') # hour | |
| 1275 if len( pieces) == 4: | |
| 1276 pieces.append( '0') # minute | |
| 1277 if len( pieces) == 5: | |
| 1278 pieces.append( '0') # second | |
| 1279 pieces = pieces[:6] + [ 0, 0, 0] | |
| 1280 time_tup = tuple( map( int, pieces)) | |
| 1281 t = calendar.timegm( time_tup) | |
| 1282 return t | |
| 1283 | |
| 1284 | |
| 1285 def time_read_date2( text): | |
| 1286 ''' | |
| 1287 Parses strings like '2y4d8h34m5s', returning seconds. | |
| 1288 | |
| 1289 Supported time periods are: | |
| 1290 s: seconds | |
| 1291 m: minutes | |
| 1292 h: hours | |
| 1293 d: days | |
| 1294 w: weeks | |
| 1295 y: years | |
| 1296 ''' | |
| 1297 #print 'text=%r' % text | |
| 1298 text0 = '' | |
| 1299 t = 0 | |
| 1300 i0 = 0 | |
| 1301 for i in range( len( text)): | |
| 1302 if text[i] in 'ywdhms': | |
| 1303 dt = int( text[i0:i]) | |
| 1304 i0=i+1 | |
| 1305 if text[i]=='s': dt *= 1 | |
| 1306 elif text[i]=='m': dt *= 60 | |
| 1307 elif text[i]=='h': dt *= 60*60 | |
| 1308 elif text[i]=='d': dt *= 60*60*24 | |
| 1309 elif text[i]=='w': dt *= 60*60*24*7 | |
| 1310 elif text[i]=='y': dt *= 60*60*24*365 | |
| 1311 t += dt | |
| 1312 return t | |
| 1313 | |
| 1314 def time_read_date3( t, origin=None): | |
| 1315 ''' | |
| 1316 Reads a date/time specification and returns absolute time in seconds. | |
| 1317 | |
| 1318 If <text> starts with '+' or '-', reads relative time with read_date2() and | |
| 1319 adds/subtracts from <origin> (or time.time() if None). | |
| 1320 | |
| 1321 Otherwise parses date/time with read_date1(). | |
| 1322 ''' | |
| 1323 if t[0] in '+-': | |
| 1324 if origin is None: | |
| 1325 origin = time.time() | |
| 1326 dt = time_read_date2( t[1:]) | |
| 1327 if t[0] == '+': | |
| 1328 return origin + dt | |
| 1329 else: | |
| 1330 return origin - dt | |
| 1331 return time_read_date1( t) | |
| 1332 | |
| 1333 | |
| 1334 def stream_prefix_time( stream): | |
| 1335 ''' | |
| 1336 Returns `StreamPrefix` that prefixes lines with time and elapsed time. | |
| 1337 ''' | |
| 1338 t_start = time.time() | |
| 1339 def prefix_time(): | |
| 1340 return '%s (+%s): ' % ( | |
| 1341 time.strftime( '%T'), | |
| 1342 time_duration( time.time() - t_start, s_format='0.1f'), | |
| 1343 ) | |
| 1344 return StreamPrefix( stream, prefix_time) | |
| 1345 | |
| 1346 def stdout_prefix_time(): | |
| 1347 ''' | |
| 1348 Changes `sys.stdout` to prefix time and elapsed time; returns original | |
| 1349 `sys.stdout`. | |
| 1350 ''' | |
| 1351 ret = sys.stdout | |
| 1352 sys.stdout = stream_prefix_time( sys.stdout) | |
| 1353 return ret | |
| 1354 | |
| 1355 | |
| 1356 def make_out_callable( out): | |
| 1357 ''' | |
| 1358 Returns a stream-like object with a `.write()` method that writes to `out`. | |
| 1359 out: | |
| 1360 | |
| 1361 * Where output is sent. | |
| 1362 * If `None`, output is lost. | |
| 1363 * Otherwise if an integer, we do: `os.write( out, text)` | |
| 1364 * Otherwise if callable, we do: `out( text)` | |
| 1365 * Otherwise we assume `out` is python stream or similar, and do: `out.write(text)` | |
| 1366 ''' | |
| 1367 class Ret: | |
| 1368 def write( self, text): | |
| 1369 pass | |
| 1370 def flush( self): | |
| 1371 pass | |
| 1372 ret = Ret() | |
| 1373 if out == log: | |
| 1374 # A hack to avoid expanding '{...}' in text, if caller | |
| 1375 # does: jlib.system(..., out=jlib.log, ...). | |
| 1376 out = lambda text: log(text, nv=False) | |
| 1377 if out is None: | |
| 1378 ret.write = lambda text: None | |
| 1379 elif isinstance( out, int): | |
| 1380 ret.write = lambda text: os.write( out, text) | |
| 1381 elif callable( out): | |
| 1382 ret.write = out | |
| 1383 else: | |
| 1384 ret.write = lambda text: out.write( text) | |
| 1385 return ret | |
| 1386 | |
| 1387 def _env_extra_text( env_extra): | |
| 1388 ret = '' | |
| 1389 if env_extra: | |
| 1390 for n, v in env_extra.items(): | |
| 1391 assert isinstance( n, str), f'env_extra has non-string name {n!r}: {env_extra!r}' | |
| 1392 assert isinstance( v, str), f'env_extra name={n!r} has non-string value {v!r}: {env_extra!r}' | |
| 1393 ret += f'{n}={shlex.quote(v)} ' | |
| 1394 return ret | |
| 1395 | |
| 1396 def command_env_text( command, env_extra): | |
| 1397 ''' | |
| 1398 Returns shell command that would run `command` with environmental settings | |
| 1399 in `env_extra`. | |
| 1400 | |
| 1401 Useful for diagnostics - the returned text can be pasted into terminal to | |
| 1402 re-run a command manually. | |
| 1403 | |
| 1404 `command` is expected to be already shell escaped, we do not escape it with | |
| 1405 `shlex.quote()`. | |
| 1406 ''' | |
| 1407 prefix = _env_extra_text( env_extra) | |
| 1408 return f'{prefix}{command}' | |
| 1409 | |
| 1410 def system( | |
| 1411 command, | |
| 1412 verbose=True, | |
| 1413 raise_errors=True, | |
| 1414 out=sys.stdout, | |
| 1415 prefix=None, | |
| 1416 shell=True, | |
| 1417 encoding='utf8', | |
| 1418 errors='replace', | |
| 1419 executable=None, | |
| 1420 caller=1, | |
| 1421 bufsize=-1, | |
| 1422 env_extra=None, | |
| 1423 multiline=True, | |
| 1424 ): | |
| 1425 ''' | |
| 1426 Runs a command like `os.system()` or `subprocess.*`, but with more | |
| 1427 flexibility. | |
| 1428 | |
| 1429 We give control over where the command's output is sent, whether to return | |
| 1430 the output and/or exit code, and whether to raise an exception if the | |
| 1431 command fails. | |
| 1432 | |
| 1433 Args: | |
| 1434 | |
| 1435 command: | |
| 1436 The command to run. | |
| 1437 verbose: | |
| 1438 If true, we write information about the command that was run, and | |
| 1439 its result, to `jlib.log()`. | |
| 1440 raise_errors: | |
| 1441 If true, we raise an exception if the command fails, otherwise we | |
| 1442 return the failing error code or zero. | |
| 1443 out: | |
| 1444 Where to send output from child process. | |
| 1445 | |
| 1446 `out` is `o` or `(o, prefix)` or list of such items. Each `o` is | |
| 1447 matched as follows: | |
| 1448 | |
| 1449 `None`: child process inherits this process's stdout and | |
| 1450 stderr. (Must be the only item, and `prefix` is not supported.) | |
| 1451 | |
| 1452 `subprocess.DEVNULL`: child process's output is lost. (Must be | |
| 1453 the only item, and `prefix` is not supported.) | |
| 1454 | |
| 1455 'return': we store the output and include it in our return | |
| 1456 value or exception. Can only be specified once. | |
| 1457 | |
| 1458 'log': we write to `jlib.log()` using our caller's stack | |
| 1459 frame. Can only be specified once. | |
| 1460 | |
| 1461 An integer: we do: `os.write(o, text)` | |
| 1462 | |
| 1463 Is callable: we do: `o(text)` | |
| 1464 | |
| 1465 Otherwise we assume `o` is python stream or similar, and do: | |
| 1466 `o.write(text)` | |
| 1467 | |
| 1468 If `prefix` is specified, it is applied to each line in the output | |
| 1469 before being sent to `o`. | |
| 1470 prefix: | |
| 1471 Default prefix for all items in `out`. Can be a string, a callable | |
| 1472 taking no args that returns a string, or an integer designating the | |
| 1473 number of spaces. | |
| 1474 shell: | |
| 1475 Passed to underlying `subprocess.Popen()` call. | |
| 1476 encoding: | |
| 1477 Specify the encoding used to translate the command's output to | |
| 1478 characters. If `None` we send bytes to items in `out`. | |
| 1479 errors: | |
| 1480 How to handle encoding errors; see docs for `codecs` module | |
| 1481 for details. Defaults to 'replace' so we never raise a | |
| 1482 `UnicodeDecodeError`. | |
| 1483 executable=None: | |
| 1484 . | |
| 1485 caller: | |
| 1486 The number of frames to look up stack when call `jlib.log()` (used | |
| 1487 for `out='log'` and `verbose`). | |
| 1488 bufsize: | |
| 1489 As `subprocess.Popen()`'s `bufsize` arg, sets buffer size | |
| 1490 when creating stdout, stderr and stdin pipes. Use 0 for | |
| 1491 unbuffered, e.g. to see login/password prompts that don't end | |
| 1492 with a newline. Default -1 means `io.DEFAULT_BUFFER_SIZE`. +1 | |
| 1493 (line-buffered) does not work because we read raw bytes and decode | |
| 1494 ourselves into string. | |
| 1495 env_extra: | |
| 1496 If not `None`, a `dict` with extra items that are added to the | |
| 1497 environment passed to the child process. | |
| 1498 multiline: | |
| 1499 If true (the default) we convert a multiline command into a single | |
| 1500 command, but preserve the multiline representation in verbose | |
| 1501 diagnostics. | |
| 1502 | |
| 1503 Returns: | |
| 1504 | |
| 1505 * If raise_errors is true: | |
| 1506 | |
| 1507 If the command failed, we raise an exception; if `out` contains | |
| 1508 'return' the exception text includes the output. | |
| 1509 | |
| 1510 Else if `out` contains 'return' we return the text output from the | |
| 1511 command. | |
| 1512 | |
| 1513 Else we return `None`. | |
| 1514 | |
| 1515 * If raise_errors is false: | |
| 1516 | |
| 1517 If `out` contains 'return', we return `(e, text)` where `e` is the | |
| 1518 command's exit code and `text` is the output from the command. | |
| 1519 | |
| 1520 Else we return `e`, the command's return code. | |
| 1521 | |
| 1522 In the above, `e` is the `subprocess`-style returncode - the exit | |
| 1523 code, or `-N` if killed by signal `N`. | |
| 1524 | |
| 1525 >>> print(system('echo hello a', prefix='foo:', out='return')) | |
| 1526 foo:hello a | |
| 1527 foo: | |
| 1528 | |
| 1529 >>> system('echo hello b', prefix='foo:', out='return', raise_errors=False) | |
| 1530 (0, 'foo:hello b\\nfoo:') | |
| 1531 | |
| 1532 >>> system('echo hello c && false', prefix='foo:', out='return', env_extra=dict(FOO='bar qwerty')) | |
| 1533 Traceback (most recent call last): | |
| 1534 Exception: Command failed: FOO='bar qwerty' echo hello c && false | |
| 1535 Output was: | |
| 1536 foo:hello c | |
| 1537 foo: | |
| 1538 <BLANKLINE> | |
| 1539 ''' | |
| 1540 out_pipe = 0 | |
| 1541 out_none = 0 | |
| 1542 out_devnull = 0 | |
| 1543 out_return = None | |
| 1544 out_log = 0 | |
| 1545 | |
| 1546 outs = out if isinstance(out, list) else [out] | |
| 1547 decoders = dict() | |
| 1548 def decoders_ensure(encoding): | |
| 1549 d = decoders.get(encoding) | |
| 1550 if d is None: | |
| 1551 class D: | |
| 1552 pass | |
| 1553 d = D() | |
| 1554 # subprocess's universal_newlines and codec.streamreader seem to | |
| 1555 # always use buffering even with bufsize=0, so they don't reliably | |
| 1556 # display prompts or other text that doesn't end with a newline. | |
| 1557 # | |
| 1558 # So we create our own incremental decode, which seems to work | |
| 1559 # better. | |
| 1560 # | |
| 1561 d.decoder = codecs.getincrementaldecoder(encoding)(errors) | |
| 1562 d.out = '' | |
| 1563 decoders[ encoding] = d | |
| 1564 return d | |
| 1565 | |
| 1566 for i, o in enumerate(outs): | |
| 1567 if o is None: | |
| 1568 out_none += 1 | |
| 1569 elif o == subprocess.DEVNULL: | |
| 1570 out_devnull += 1 | |
| 1571 else: | |
| 1572 out_pipe += 1 | |
| 1573 o_prefix = prefix | |
| 1574 if isinstance(o, tuple) and len(o) == 2: | |
| 1575 o, o_prefix = o | |
| 1576 assert o not in (None, subprocess.DEVNULL), f'out[]={o} does not make sense with a prefix ({o_prefix})' | |
| 1577 assert not isinstance(o, (tuple, list)) | |
| 1578 o_decoder = None | |
| 1579 if o == 'return': | |
| 1580 assert not out_return, f'"return" specified twice does not make sense' | |
| 1581 out_return = io.StringIO() | |
| 1582 o_fn = out_return.write | |
| 1583 elif o == 'log': | |
| 1584 assert not out_log, f'"log" specified twice does not make sense' | |
| 1585 out_log += 1 | |
| 1586 out_frame_record = inspect.stack()[caller] | |
| 1587 o_fn = lambda text: log( text, caller=out_frame_record, nv=False, raw=True) | |
| 1588 elif isinstance(o, int): | |
| 1589 def fn(text, o=o): | |
| 1590 os.write(o, text.encode()) | |
| 1591 o_fn = fn | |
| 1592 elif callable(o): | |
| 1593 o_fn = o | |
| 1594 else: | |
| 1595 assert hasattr(o, 'write') and callable(o.write), ( | |
| 1596 f'Do not understand o={o}, must be one of:' | |
| 1597 ' None, subprocess.DEVNULL, "return", "log", <int>,' | |
| 1598 ' or support o() or o.write().' | |
| 1599 ) | |
| 1600 o_decoder = decoders_ensure(o.encoding) | |
| 1601 def o_fn(text, o=o): | |
| 1602 if errors == 'strict': | |
| 1603 o.write(text) | |
| 1604 else: | |
| 1605 # This is probably only necessary on Windows, where | |
| 1606 # sys.stdout can be cp1252 and will sometimes raise | |
| 1607 # UnicodeEncodeError. We hard-ignore these errors. | |
| 1608 try: | |
| 1609 o.write(text) | |
| 1610 except Exception as e: | |
| 1611 o.write(f'\n[Ignoring Exception: {e}]\n') | |
| 1612 o.flush() # Seems to be necessary on Windows. | |
| 1613 if o_prefix: | |
| 1614 o_fn = StreamPrefix( o_fn, o_prefix).write | |
| 1615 if not o_decoder: | |
| 1616 o_decoder = decoders_ensure(encoding) | |
| 1617 outs[i] = o_fn, o_decoder | |
| 1618 | |
| 1619 if out_pipe: | |
| 1620 stdout = subprocess.PIPE | |
| 1621 stderr = subprocess.STDOUT | |
| 1622 elif out_none == len(outs): | |
| 1623 stdout = None | |
| 1624 stderr = None | |
| 1625 elif out_devnull == len(outs): | |
| 1626 stdout = subprocess.DEVNULL | |
| 1627 stderr = subprocess.DEVNULL | |
| 1628 else: | |
| 1629 assert 0, f'Inconsistent out: {out}' | |
| 1630 | |
| 1631 if multiline and '\n' in command: | |
| 1632 command = textwrap.dedent(command) | |
| 1633 lines = list() | |
| 1634 for line in command.split( '\n'): | |
| 1635 h = 0 if line.startswith( '#') else line.find(' #') | |
| 1636 if h >= 0: | |
| 1637 line = line[:h] | |
| 1638 if line.strip(): | |
| 1639 line = line.rstrip() | |
| 1640 lines.append(line) | |
| 1641 sep = ' ' if platform.system() == 'Windows' else ' \\\n' | |
| 1642 command = sep.join(lines) | |
| 1643 | |
| 1644 if verbose: | |
| 1645 log(f'running: {command_env_text( command, env_extra)}', nv=0, caller=caller+1) | |
| 1646 | |
| 1647 env = None | |
| 1648 if env_extra: | |
| 1649 env = os.environ.copy() | |
| 1650 env.update(env_extra) | |
| 1651 | |
| 1652 child = subprocess.Popen( | |
| 1653 command, | |
| 1654 shell=shell, | |
| 1655 stdin=None, | |
| 1656 stdout=stdout, | |
| 1657 stderr=stderr, | |
| 1658 close_fds=True, | |
| 1659 executable=executable, | |
| 1660 bufsize=bufsize, | |
| 1661 env=env | |
| 1662 ) | |
| 1663 | |
| 1664 if out_pipe: | |
| 1665 while 1: | |
| 1666 # os.read() seems to be better for us than child.stdout.read() | |
| 1667 # because it returns a short read if data is not available. Where | |
| 1668 # as child.stdout.read() appears to be more willing to wait for | |
| 1669 # data until the requested number of bytes have been received. | |
| 1670 # | |
| 1671 # Also, os.read() does the right thing if the sender has made | |
| 1672 # multiple calls to write() - it returns all available data, not | |
| 1673 # just from the first unread write() call. | |
| 1674 # | |
| 1675 output0 = os.read( child.stdout.fileno(), 10000) | |
| 1676 final = not output0 | |
| 1677 for _, decoder in decoders.items(): | |
| 1678 decoder.out = decoder.decoder.decode(output0, final) | |
| 1679 for o_fn, o_decoder in outs: | |
| 1680 o_fn( o_decoder.out) | |
| 1681 if not output0: | |
| 1682 break | |
| 1683 | |
| 1684 e = child.wait() | |
| 1685 | |
| 1686 if out_log: | |
| 1687 global _log_text_line_start | |
| 1688 if not _log_text_line_start: | |
| 1689 # Terminate last incomplete line of log outputs. | |
| 1690 sys.stdout.write('\n') | |
| 1691 _log_text_line_start = True | |
| 1692 if verbose: | |
| 1693 log(f'[returned e={e}]', nv=0, caller=caller+1) | |
| 1694 | |
| 1695 if out_return: | |
| 1696 out_return = out_return.getvalue() | |
| 1697 | |
| 1698 if raise_errors: | |
| 1699 if e: | |
| 1700 message = f'Command failed: {command_env_text( command, env_extra)}' | |
| 1701 if out_return is not None: | |
| 1702 if not out_return.endswith('\n'): | |
| 1703 out_return += '\n' | |
| 1704 raise Exception( | |
| 1705 message + '\n' | |
| 1706 + 'Output was:\n' | |
| 1707 + out_return | |
| 1708 ) | |
| 1709 else: | |
| 1710 raise Exception( message) | |
| 1711 elif out_return is not None: | |
| 1712 return out_return | |
| 1713 else: | |
| 1714 return | |
| 1715 | |
| 1716 if out_return is not None: | |
| 1717 return e, out_return | |
| 1718 else: | |
| 1719 return e | |
| 1720 | |
| 1721 | |
| 1722 def system_rusage( | |
| 1723 command, | |
| 1724 verbose=None, | |
| 1725 raise_errors=True, | |
| 1726 out=sys.stdout, | |
| 1727 prefix=None, | |
| 1728 rusage=False, | |
| 1729 shell=True, | |
| 1730 encoding='utf8', | |
| 1731 errors='replace', | |
| 1732 executable=None, | |
| 1733 caller=1, | |
| 1734 bufsize=-1, | |
| 1735 env_extra=None, | |
| 1736 ): | |
| 1737 ''' | |
| 1738 Old code that gets timing info; probably doesn't work. | |
| 1739 ''' | |
| 1740 command2 = '' | |
| 1741 command2 += '/usr/bin/time -o ubt-out -f "D=%D E=%D F=%F I=%I K=%K M=%M O=%O P=%P R=%r S=%S U=%U W=%W X=%X Z=%Z c=%c e=%e k=%k p=%p r=%r s=%s t=%t w=%w x=%x C=%C"' | |
| 1742 command2 += ' ' | |
| 1743 command2 += command | |
| 1744 | |
| 1745 e = system( | |
| 1746 command2, | |
| 1747 out, | |
| 1748 shell, | |
| 1749 encoding, | |
| 1750 errors, | |
| 1751 executable=executable, | |
| 1752 ) | |
| 1753 if e: | |
| 1754 raise Exception('/usr/bin/time failed') | |
| 1755 with open('ubt-out') as f: | |
| 1756 rusage_text = f.read() | |
| 1757 #print 'have read rusage output: %r' % rusage_text | |
| 1758 if rusage_text.startswith( 'Command '): | |
| 1759 # Annoyingly, /usr/bin/time appears to write 'Command | |
| 1760 # exited with ...' or 'Command terminated by ...' to the | |
| 1761 # output file before the rusage info if command doesn't | |
| 1762 # exit 0. | |
| 1763 nl = rusage_text.find('\n') | |
| 1764 rusage_text = rusage_text[ nl+1:] | |
| 1765 return rusage_text | |
| 1766 | |
| 1767 | |
| 1768 def git_get_files( directory, submodules=False, relative=True): | |
| 1769 ''' | |
| 1770 Returns list of all files known to git in `directory`; `directory` must be | |
| 1771 somewhere within a git checkout. | |
| 1772 | |
| 1773 Returned names are all relative to `directory`. | |
| 1774 | |
| 1775 If `<directory>.git` exists we use git-ls-files and write list of files to | |
| 1776 `<directory>/jtest-git-files`. | |
| 1777 | |
| 1778 Otherwise we require that `<directory>/jtest-git-files` already exists. | |
| 1779 ''' | |
| 1780 def is_within_git_checkout( d): | |
| 1781 while 1: | |
| 1782 #log( '{d=}') | |
| 1783 if not d or d=='/': | |
| 1784 break | |
| 1785 if os.path.isdir( f'{d}/.git'): | |
| 1786 return True | |
| 1787 d = os.path.dirname( d) | |
| 1788 | |
| 1789 ret = [] | |
| 1790 if is_within_git_checkout( directory): | |
| 1791 command = 'cd ' + directory + ' && git ls-files' | |
| 1792 if submodules: | |
| 1793 command += ' --recurse-submodules' | |
| 1794 command += ' > jtest-git-files' | |
| 1795 system( command, verbose=False) | |
| 1796 with open( '%s/jtest-git-files' % directory, 'r') as f: | |
| 1797 text = f.read() | |
| 1798 for p in text.strip().split( '\n'): | |
| 1799 if not relative: | |
| 1800 p = os.path.join( directory, p) | |
| 1801 ret.append( p) | |
| 1802 return ret | |
| 1803 | |
| 1804 def git_get_id_raw( directory): | |
| 1805 if not os.path.isdir( '%s/.git' % directory): | |
| 1806 return | |
| 1807 text = system( | |
| 1808 f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)', | |
| 1809 out='return', | |
| 1810 ) | |
| 1811 return text | |
| 1812 | |
| 1813 def git_get_id( directory, allow_none=False): | |
| 1814 ''' | |
| 1815 Returns text where first line is '<git-sha> <commit summary>' and remaining | |
| 1816 lines contain output from 'git diff' in <directory>. | |
| 1817 | |
| 1818 directory: | |
| 1819 Root of git checkout. | |
| 1820 allow_none: | |
| 1821 If true, we return None if `directory` is not a git checkout and | |
| 1822 jtest-git-id file does not exist. | |
| 1823 ''' | |
| 1824 filename = f'{directory}/jtest-git-id' | |
| 1825 text = git_get_id_raw( directory) | |
| 1826 if text: | |
| 1827 with open( filename, 'w') as f: | |
| 1828 f.write( text) | |
| 1829 elif os.path.isfile( filename): | |
| 1830 with open( filename) as f: | |
| 1831 text = f.read() | |
| 1832 else: | |
| 1833 if not allow_none: | |
| 1834 raise Exception( f'Not in git checkout, and no file called: {filename}.') | |
| 1835 text = None | |
| 1836 return text | |
| 1837 | |
| 1838 class Args: | |
| 1839 ''' | |
| 1840 Iterates over argv items. | |
| 1841 ''' | |
| 1842 def __init__( self, argv): | |
| 1843 self.items = iter( argv) | |
| 1844 def next( self): | |
| 1845 if sys.version_info[0] == 3: | |
| 1846 return next( self.items) | |
| 1847 else: | |
| 1848 return self.items.next() | |
| 1849 def next_or_none( self): | |
| 1850 try: | |
| 1851 return self.next() | |
| 1852 except StopIteration: | |
| 1853 return None | |
| 1854 | |
| 1855 | |
| 1856 def fs_read( path, binary=False): | |
| 1857 with open( path, 'rb' if binary else 'r') as f: | |
| 1858 return f.read() | |
| 1859 | |
| 1860 def fs_write( path, data, binary=False): | |
| 1861 with open( path, 'wb' if binary else 'w') as f: | |
| 1862 return f.write( data) | |
| 1863 | |
| 1864 | |
| 1865 def fs_update( text, filename, return_different=False): | |
| 1866 ''' | |
| 1867 Writes `text` to `filename`. Does nothing if contents of `filename` are | |
| 1868 already `text`. | |
| 1869 | |
| 1870 If `return_different` is true, we return existing contents if `filename` | |
| 1871 already exists and differs from `text`. | |
| 1872 | |
| 1873 Otherwise we return true if file has changed. | |
| 1874 ''' | |
| 1875 try: | |
| 1876 with open( filename) as f: | |
| 1877 text0 = f.read() | |
| 1878 except OSError: | |
| 1879 text0 = None | |
| 1880 if text != text0: | |
| 1881 if return_different and text0 is not None: | |
| 1882 return text | |
| 1883 # Write to temp file and rename, to ensure we are atomic. | |
| 1884 filename_temp = f'{filename}-jlib-temp' | |
| 1885 with open( filename_temp, 'w') as f: | |
| 1886 f.write( text) | |
| 1887 fs_rename( filename_temp, filename) | |
| 1888 return True | |
| 1889 | |
| 1890 | |
| 1891 def fs_find_in_paths( name, paths=None, verbose=False): | |
| 1892 ''' | |
| 1893 Looks for `name` in paths and returns complete path. `paths` is list/tuple | |
| 1894 or `os.pathsep`-separated string; if `None` we use `$PATH`. If `name` | |
| 1895 contains `/`, we return `name` itself if it is a file, regardless of $PATH. | |
| 1896 ''' | |
| 1897 if '/' in name: | |
| 1898 return name if os.path.isfile( name) else None | |
| 1899 if paths is None: | |
| 1900 paths = os.environ.get( 'PATH', '') | |
| 1901 if verbose: | |
| 1902 log('From os.environ["PATH"]: {paths=}') | |
| 1903 if isinstance( paths, str): | |
| 1904 paths = paths.split( os.pathsep) | |
| 1905 if verbose: | |
| 1906 log('After split: {paths=}') | |
| 1907 for path in paths: | |
| 1908 p = os.path.join( path, name) | |
| 1909 if verbose: | |
| 1910 log('Checking {p=}') | |
| 1911 if os.path.isfile( p): | |
| 1912 if verbose: | |
| 1913 log('Returning because is file: {p!r}') | |
| 1914 return p | |
| 1915 if verbose: | |
| 1916 log('Returning None because not found: {name!r}') | |
| 1917 | |
| 1918 | |
| 1919 def fs_mtime( filename, default=0): | |
| 1920 ''' | |
| 1921 Returns mtime of file, or `default` if error - e.g. doesn't exist. | |
| 1922 ''' | |
| 1923 try: | |
| 1924 return os.path.getmtime( filename) | |
| 1925 except OSError: | |
| 1926 return default | |
| 1927 | |
| 1928 | |
| 1929 def fs_filesize( filename, default=0): | |
| 1930 try: | |
| 1931 return os.path.getsize( filename) | |
| 1932 except OSError: | |
| 1933 return default | |
| 1934 | |
| 1935 | |
| 1936 def fs_paths( paths): | |
| 1937 ''' | |
| 1938 Yields each file in `paths`, walking any directories. | |
| 1939 | |
| 1940 If `paths` is a tuple `(paths2, filter_)` and `filter_` is callable, we | |
| 1941 yield all files in `paths2` for which `filter_(path2)` returns true. | |
| 1942 ''' | |
| 1943 filter_ = lambda path: True | |
| 1944 if isinstance( paths, tuple) and len( paths) == 2 and callable( paths[1]): | |
| 1945 paths, filter_ = paths | |
| 1946 if isinstance( paths, str): | |
| 1947 paths = (paths,) | |
| 1948 for name in paths: | |
| 1949 if os.path.isdir( name): | |
| 1950 for dirpath, dirnames, filenames in os.walk( name): | |
| 1951 for filename in filenames: | |
| 1952 path = os.path.join( dirpath, filename) | |
| 1953 if filter_( path): | |
| 1954 yield path | |
| 1955 else: | |
| 1956 if filter_( name): | |
| 1957 yield name | |
| 1958 | |
| 1959 def fs_remove( path, backup=False): | |
| 1960 ''' | |
| 1961 Removes file or directory, without raising exception if it doesn't exist. | |
| 1962 | |
| 1963 path: | |
| 1964 The path to remove. | |
| 1965 backup: | |
| 1966 If true, we rename any existing file/directory called `path` to | |
| 1967 `<path>-<datetime>`. | |
| 1968 | |
| 1969 We assert-fail if the path still exists when we return, in case of | |
| 1970 permission problems etc. | |
| 1971 ''' | |
| 1972 if backup and os.path.exists( path): | |
| 1973 datetime = date_time() | |
| 1974 if platform.system() == 'Windows' or platform.system().startswith( 'CYGWIN'): | |
| 1975 # os.rename() fails if destination contains colons, with: | |
| 1976 # [WinError87] The parameter is incorrect ... | |
| 1977 datetime = datetime.replace( ':', '') | |
| 1978 p = f'{path}-{datetime}' | |
| 1979 log( 'Moving out of way: {path} => {p}') | |
| 1980 os.rename( path, p) | |
| 1981 try: | |
| 1982 os.remove( path) | |
| 1983 except Exception: | |
| 1984 pass | |
| 1985 shutil.rmtree( path, ignore_errors=1) | |
| 1986 assert not os.path.exists( path) | |
| 1987 | |
| 1988 def fs_remove_dir_contents( path): | |
| 1989 ''' | |
| 1990 Removes all items in directory `path`; does not remove `path` itself. | |
| 1991 ''' | |
| 1992 for leaf in os.listdir( path): | |
| 1993 path2 = os.path.join( path, leaf) | |
| 1994 fs_remove(path2) | |
| 1995 | |
| 1996 def fs_ensure_empty_dir( path): | |
| 1997 os.makedirs( path, exist_ok=True) | |
| 1998 fs_remove_dir_contents( path) | |
| 1999 | |
| 2000 def fs_rename(src, dest): | |
| 2001 ''' | |
| 2002 Renames `src` to `dest`. If we get an error, we try to remove `dest` | |
| 2003 explicitly and then retry; this is to make things work on Windows. | |
| 2004 ''' | |
| 2005 try: | |
| 2006 os.rename(src, dest) | |
| 2007 except Exception: | |
| 2008 os.remove(dest) | |
| 2009 os.rename(src, dest) | |
| 2010 | |
| 2011 def fs_copy(src, dest, verbose=False): | |
| 2012 ''' | |
| 2013 Wrapper for `shutil.copy()` that also ensures parent of `dest` exists and | |
| 2014 optionally calls `jlib.log()` with diagnostic. | |
| 2015 ''' | |
| 2016 if verbose: | |
| 2017 log('Copying {src} to {dest}') | |
| 2018 dirname = os.path.dirname(dest) | |
| 2019 if dirname: | |
| 2020 os.makedirs( dirname, exist_ok=True) | |
| 2021 shutil.copy2( src, dest) | |
| 2022 | |
| 2023 | |
| 2024 def untar(path, mode='r:gz', prefix=None): | |
| 2025 ''' | |
| 2026 Extracts tar file. | |
| 2027 | |
| 2028 We fail if items in tar file have different top-level directory names, or | |
| 2029 if tar file's top-level directory name already exists locally. | |
| 2030 | |
| 2031 path: | |
| 2032 The tar file. | |
| 2033 mode: | |
| 2034 As `tarfile.open()`. | |
| 2035 prefix: | |
| 2036 If not `None`, we fail if tar file's top-level directory name is not | |
| 2037 `prefix`. | |
| 2038 | |
| 2039 Returns the directory name (which will be `prefix` if not `None`). | |
| 2040 ''' | |
| 2041 with tarfile.open( path, mode) as t: | |
| 2042 items = t.getnames() | |
| 2043 assert items | |
| 2044 item = items[0] | |
| 2045 assert not item.startswith('.') | |
| 2046 s = item.find('/') | |
| 2047 if s == -1: | |
| 2048 prefix_actual = item + '/' | |
| 2049 else: | |
| 2050 prefix_actual = item[:s+1] | |
| 2051 if prefix: | |
| 2052 assert prefix == prefix_actual, f'prefix={prefix} prefix_actual={prefix_actual}' | |
| 2053 for item in items[1:]: | |
| 2054 assert item.startswith( prefix_actual), f'prefix_actual={prefix_actual!r} != item={item!r}' | |
| 2055 assert not os.path.exists( prefix_actual) | |
| 2056 t.extractall() | |
| 2057 return prefix_actual | |
| 2058 | |
| 2059 | |
| 2060 # Things for figuring out whether files need updating, using mtimes. | |
| 2061 # | |
| 2062 def fs_newest( names): | |
| 2063 ''' | |
| 2064 Returns mtime of newest file in `filenames`. Returns 0 if no file exists. | |
| 2065 ''' | |
| 2066 assert isinstance( names, (list, tuple)) | |
| 2067 assert names | |
| 2068 ret_t = 0 | |
| 2069 ret_name = None | |
| 2070 for filename in fs_paths( names): | |
| 2071 if filename.endswith('.pyc'): | |
| 2072 continue | |
| 2073 t = fs_mtime( filename) | |
| 2074 if t > ret_t: | |
| 2075 ret_t = t | |
| 2076 ret_name = filename | |
| 2077 return ret_t, ret_name | |
| 2078 | |
| 2079 def fs_oldest( names): | |
| 2080 ''' | |
| 2081 Returns mtime of oldest file in `filenames` or 0 if no file exists. | |
| 2082 ''' | |
| 2083 assert isinstance( names, (list, tuple)) | |
| 2084 assert names | |
| 2085 ret_t = None | |
| 2086 ret_name = None | |
| 2087 for filename in fs_paths( names): | |
| 2088 t = fs_mtime( filename) | |
| 2089 if ret_t is None or t < ret_t: | |
| 2090 ret_t = t | |
| 2091 ret_name = filename | |
| 2092 if ret_t is None: | |
| 2093 ret_t = 0 | |
| 2094 return ret_t, ret_name | |
| 2095 | |
| 2096 def fs_any_newer( infiles, outfiles): | |
| 2097 ''' | |
| 2098 If any file in `infiles` is newer than any file in `outfiles`, returns | |
| 2099 string description. Otherwise returns `None`. | |
| 2100 ''' | |
| 2101 in_tmax, in_tmax_name = fs_newest( infiles) | |
| 2102 out_tmin, out_tmin_name = fs_oldest( outfiles) | |
| 2103 if in_tmax > out_tmin: | |
| 2104 text = f'{in_tmax_name} is newer than {out_tmin_name}' | |
| 2105 return text | |
| 2106 | |
| 2107 def fs_ensure_parent_dir( path): | |
| 2108 parent = os.path.dirname( path) | |
| 2109 if parent: | |
| 2110 os.makedirs( parent, exist_ok=True) | |
| 2111 | |
| 2112 def fs_newer( pattern, t): | |
| 2113 ''' | |
| 2114 Returns list of files matching glob `pattern` whose mtime is >= `t`. | |
| 2115 ''' | |
| 2116 paths = glob.glob(pattern) | |
| 2117 paths_new = [] | |
| 2118 for path in paths: | |
| 2119 tt = fs_mtime(path) | |
| 2120 if tt >= t: | |
| 2121 paths_new.append(path) | |
| 2122 return paths_new | |
| 2123 | |
| 2124 def build( | |
| 2125 infiles, | |
| 2126 outfiles, | |
| 2127 command, | |
| 2128 force_rebuild=False, | |
| 2129 out=None, | |
| 2130 all_reasons=False, | |
| 2131 verbose=True, | |
| 2132 executable=None, | |
| 2133 ): | |
| 2134 ''' | |
| 2135 Ensures that `outfiles` are up to date using enhanced makefile-like | |
| 2136 determinism of dependencies. | |
| 2137 | |
| 2138 Rebuilds `outfiles` by running `command` if we determine that any of them | |
| 2139 are out of date, or if `command` has changed. | |
| 2140 | |
| 2141 infiles: | |
| 2142 Names of files that are read by `command`. Can be a single filename. If | |
| 2143 an item is a directory, we expand to all filenames in the directory's | |
| 2144 tree. Can be `(files2, filter_)` as supported by `jlib.fs_paths()`. | |
| 2145 outfiles: | |
| 2146 Names of files that are written by `command`. Can also be a single | |
| 2147 filename. Can be `(files2, filter_)` as supported by `jlib.fs_paths()`. | |
| 2148 command: | |
| 2149 Command to run. {IN} and {OUT} are replaced by space-separated | |
| 2150 `infiles` and `outfiles` with '/' changed to '\' on Windows. | |
| 2151 force_rebuild: | |
| 2152 If true, we always re-run the command. | |
| 2153 out: | |
| 2154 A callable, passed to `jlib.system()`. If `None`, we use `jlib.log()` | |
| 2155 with our caller's stack record (by passing `(out='log', caller=2)` to | |
| 2156 `jlib.system()`). | |
| 2157 all_reasons: | |
| 2158 If true we check all ways for a build being needed, even if we already | |
| 2159 know a build is needed; this only affects the diagnostic that we | |
| 2160 output. | |
| 2161 verbose: | |
| 2162 Passed to `jlib.system()`. | |
| 2163 | |
| 2164 Returns: | |
| 2165 true if we have run the command, otherwise None. | |
| 2166 | |
| 2167 We compare mtimes of `infiles` and `outfiles`, and we also detect changes | |
| 2168 to the command itself. | |
| 2169 | |
| 2170 If any of infiles are newer than any of `outfiles`, or `command` is | |
| 2171 different to contents of commandfile `<outfile[0]>.cmd`, then truncates | |
| 2172 commandfile and runs `command`. If `command` succeeds we writes `command` | |
| 2173 to commandfile. | |
| 2174 ''' | |
| 2175 if isinstance( infiles, str): | |
| 2176 infiles = (infiles,) | |
| 2177 if isinstance( outfiles, str): | |
| 2178 outfiles = (outfiles,) | |
| 2179 | |
| 2180 if out is None: | |
| 2181 out = 'log' | |
| 2182 | |
| 2183 command_filename = f'{outfiles[0]}.cmd' | |
| 2184 reasons = [] | |
| 2185 | |
| 2186 if not reasons or all_reasons: | |
| 2187 if force_rebuild: | |
| 2188 reasons.append( 'force_rebuild was specified') | |
| 2189 | |
| 2190 os_name = platform.system() | |
| 2191 os_windows = (os_name == 'Windows' or os_name.startswith('CYGWIN')) | |
| 2192 def files_string(files): | |
| 2193 if isinstance(files, tuple) and len(files) == 2 and callable(files[1]): | |
| 2194 files = files[0], | |
| 2195 ret = ' '.join(files) | |
| 2196 if os_windows: | |
| 2197 # This works on Cygwyn; we might only need '\\' if running in a Cmd | |
| 2198 # window. | |
| 2199 ret = ret.replace('/', '\\\\') | |
| 2200 return ret | |
| 2201 command = command.replace('{IN}', files_string(infiles)) | |
| 2202 command = command.replace('{OUT}', files_string(outfiles)) | |
| 2203 | |
| 2204 if not reasons or all_reasons: | |
| 2205 try: | |
| 2206 with open( command_filename) as f: | |
| 2207 command0 = f.read() | |
| 2208 except Exception: | |
| 2209 command0 = None | |
| 2210 if command != command0: | |
| 2211 reasons.append( f'command has changed:\n{command0}\n=>\n{command}') | |
| 2212 | |
| 2213 if not reasons or all_reasons: | |
| 2214 reason = fs_any_newer( infiles, outfiles) | |
| 2215 if reason: | |
| 2216 reasons.append( reason) | |
| 2217 | |
| 2218 if not reasons: | |
| 2219 log( 'Already up to date: ' + ' '.join(outfiles), caller=2, nv=0) | |
| 2220 return | |
| 2221 | |
| 2222 log( f'Rebuilding because {", and ".join(reasons)}: {" ".join(outfiles)}', | |
| 2223 caller=2, | |
| 2224 nv=0, | |
| 2225 ) | |
| 2226 | |
| 2227 # Empty <command_filename) while we run the command so that if command | |
| 2228 # fails but still creates target(s), then next time we will know target(s) | |
| 2229 # are not up to date. | |
| 2230 # | |
| 2231 # We rename the command to a temporary file and then rename back again | |
| 2232 # after the command finishes so that its mtime is unchanged if the command | |
| 2233 # has not changed. | |
| 2234 # | |
| 2235 fs_ensure_parent_dir( command_filename) | |
| 2236 command_filename_temp = command_filename + '-' | |
| 2237 fs_remove(command_filename_temp) | |
| 2238 if os.path.exists( command_filename): | |
| 2239 fs_rename(command_filename, command_filename_temp) | |
| 2240 fs_update( command, command_filename_temp) | |
| 2241 assert os.path.isfile( command_filename_temp) | |
| 2242 | |
| 2243 system( command, out=out, verbose=verbose, executable=executable, caller=2) | |
| 2244 | |
| 2245 assert os.path.isfile( command_filename_temp), \ | |
| 2246 f'Command seems to have deleted {command_filename_temp=}: {command!r}' | |
| 2247 | |
| 2248 fs_rename( command_filename_temp, command_filename) | |
| 2249 | |
| 2250 return True | |
| 2251 | |
| 2252 | |
| 2253 def link_l_flags( sos, ld_origin=None): | |
| 2254 ''' | |
| 2255 Returns link flags suitable for linking with each .so in <sos>. | |
| 2256 | |
| 2257 We return -L flags for each unique parent directory and -l flags for each | |
| 2258 leafname. | |
| 2259 | |
| 2260 In addition on non-Windows we append " -Wl,-rpath,'$ORIGIN,-z,origin" | |
| 2261 so that libraries will be searched for next to each other. This can be | |
| 2262 disabled by setting ld_origin to false. | |
| 2263 ''' | |
| 2264 darwin = (platform.system() == 'Darwin') | |
| 2265 dirs = set() | |
| 2266 names = [] | |
| 2267 if isinstance( sos, str): | |
| 2268 sos = [sos] | |
| 2269 ret = '' | |
| 2270 for so in sos: | |
| 2271 if not so: | |
| 2272 continue | |
| 2273 dir_ = os.path.dirname( so) | |
| 2274 name = os.path.basename( so) | |
| 2275 assert name.startswith( 'lib'), f'name={name}' | |
| 2276 m = re.search( '(.so[.0-9]*)$', name) | |
| 2277 if m: | |
| 2278 l = len(m.group(1)) | |
| 2279 dirs.add( dir_) | |
| 2280 names.append( f'-l {name[3:-l]}') | |
| 2281 elif darwin and name.endswith( '.dylib'): | |
| 2282 dirs.add( dir_) | |
| 2283 names.append( f'-l {name[3:-6]}') | |
| 2284 elif name.endswith( '.a'): | |
| 2285 names.append( so) | |
| 2286 else: | |
| 2287 assert 0, f'leaf does not end in .so or .a: {so}' | |
| 2288 ret = '' | |
| 2289 # Important to use sorted() here, otherwise ordering from set() is | |
| 2290 # arbitrary causing occasional spurious rebuilds. | |
| 2291 for dir_ in sorted(dirs): | |
| 2292 ret += f' -L {os.path.relpath(dir_)}' | |
| 2293 for name in names: | |
| 2294 ret += f' {name}' | |
| 2295 if ld_origin is None: | |
| 2296 if platform.system() != 'Windows': | |
| 2297 ld_origin = True | |
| 2298 if ld_origin: | |
| 2299 if darwin: | |
| 2300 # As well as this link flag, it is also necessary to use | |
| 2301 # `install_name_tool -change` to rename internal names to | |
| 2302 # `@rpath/<leafname>`. | |
| 2303 ret += ' -Wl,-rpath,@loader_path/.' | |
| 2304 else: | |
| 2305 ret += " -Wl,-rpath,'$ORIGIN',-z,origin" | |
| 2306 #log('{sos=} {ld_origin=} {ret=}') | |
| 2307 return ret.strip() |
