from __future__ import print_function import codecs import inspect import io import os import shutil import subprocess import sys import time import traceback import threading def place( frame_record): ''' Useful debugging function - returns representation of source position of caller. ''' filename = frame_record.filename line = frame_record.lineno function = frame_record.function ret = os.path.split( filename)[1] + ':' + str( line) + ':' + function + ':' if 0: tid = str(threading.currentThread()) ret = '[' + tid + '] ' + ret return ret def expand_nv( text, caller): ''' Returns with special handling of {} items. text: String containing {} items. caller: If an int, the number of frames to step up when looking for file:line information or evaluating expressions. Otherwise should be a frame record as returned by inspect.stack()[]. is evaluated in 's context using eval(), and expanded to or =. If ends with '=', this character is removed and we prefix the result with =. E.g.: x = 45 y = 'hello' expand_nv( 'foo {x} {y=}') returns: foo 45 y=hello can also use ':' and '!' to control formatting, like str.format(). ''' if isinstance( caller, int): frame_record = inspect.stack()[ caller] else: frame_record = caller frame = frame_record.frame try: def get_items(): ''' Yields (pre, item), where is contents of next {...} or None, and
 is preceding text.
            '''
            pos = 0
            pre = ''
            while 1:
                if pos == len( text):
                    yield pre, None
                    break
                rest = text[ pos:]
                if rest.startswith( '{{') or rest.startswith( '}}'):
                    pre += rest[0]
                    pos += 2
                elif text[ pos] == '{':
                    close = text.find( '}', pos)
                    if close < 0:
                        raise Exception( 'After "{" at offset %s, cannot find closing "}". text is: %r' % (
                                pos, text))
                    yield pre, text[ pos+1 : close]
                    pre = ''
                    pos = close + 1
                else:
                    pre += text[ pos]
                    pos += 1

        ret = ''
        for pre, item in get_items():
            ret += pre
            nv = False
            if item:
                if item.endswith( '='):
                    nv = True
                    item = item[:-1]
                expression, tail = split_first_of( item, '!:')
                try:
                    value = eval( expression, frame.f_globals, frame.f_locals)
                    value_text = ('{0%s}' % tail).format( value)
                except Exception as e:
                    value_text = '{??Failed to evaluate %r in context %s:%s because: %s??}' % (
                            expression,
                            frame_record.filename,
                            frame_record.lineno,
                            e,
                            )
                if nv:
                    ret += '%s=' % expression
                ret += value_text

        return ret

    finally:
        del frame


class LogPrefixTime:
    def __init__( self, date=False, time_=True, elapsed=False):
        self.date = date
        self.time = time_
        self.elapsed = elapsed
        self.t0 = time.time()
    def __call__( self):
        ret = ''
        if self.date:
            ret += time.strftime( ' %F')
        if self.time:
            ret += time.strftime( ' %T')
        if self.elapsed:
            ret += ' (+%s)' % time_duration( time.time() - self.t0, s_format='%.1f')
        if ret:
            ret = ret.strip() + ': '
        return ret

class LogPrefixFileLine:
    def __call__( self, caller):
        if isinstance( caller, int):
            caller = inspect.stack()[ caller]
        return place( caller) + ' '

class LogPrefixScopes:
    '''
    Internal use only.
    '''
    def __init__( self):
        self.items = []
    def __call__( self):
        ret = ''
        for item in self.items:
            if callable( item):
                item = item()
            ret += item
        return ret


class LogPrefixScope:
    '''
    Can be used to insert scoped prefix to log output.
    '''
    def __init__( self, prefix):
        g_log_prefixe_scopes.items.append( prefix)
    def __enter__( self):
        pass
    def __exit__( self, exc_type, exc_value, traceback):
        global g_log_prefix
        g_log_prefixe_scopes.items.pop()


g_log_delta = 0

class LogDeltaScope:
    '''
    Can be used to temporarily change verbose level of logging.

    E.g to temporarily increase logging:

        with jlib.LogDeltaScope(-1):
            ...
    '''
    def __init__( self, delta):
        self.delta = delta
        global g_log_delta
        g_log_delta += self.delta
    def __enter__( self):
        pass
    def __exit__( self, exc_type, exc_value, traceback):
        global g_log_delta
        g_log_delta -= self.delta

# Special item that can be inserted into  to enable
# temporary addition of text into log prefixes.
#
g_log_prefixe_scopes = LogPrefixScopes()

# List of items that form prefix for all output from log().
#
g_log_prefixes = []


def log_text( text=None, caller=1, nv=True):
    '''
    Returns log text, prepending all lines with text from g_log_prefixes.

    text:
        The text to output. Each line is prepended with prefix text.
    caller:
        If an int, the number of frames to step up when looking for file:line
        information or evaluating expressions.

        Otherwise should be a frame record as returned by inspect.stack()[].
    nv:
        If true, we expand {...} in  using expand_nv().
    '''
    if isinstance( caller, int):
        caller += 1
    prefix = ''
    for p in g_log_prefixes:
        if callable( p):
            if isinstance( p, LogPrefixFileLine):
                p = p(caller)
            else:
                p = p()
        prefix += p

    if text is None:
        return prefix

    if nv:
        text = expand_nv( text, caller)

    if text.endswith( '\n'):
        text = text[:-1]
    lines = text.split( '\n')

    text = ''
    for line in lines:
        text += prefix + line + '\n'
    return text



s_log_levels_cache = dict()
s_log_levels_items = []

def log_levels_find( caller):
    if not s_log_levels_items:
        return 0

    tb = traceback.extract_stack( None, 1+caller)
    if len(tb) == 0:
        return 0
    filename, line, function, text = tb[0]

    key = function, filename, line,
    delta = s_log_levels_cache.get( key)

    if delta is None:
        # Calculate and populate cache.
        delta = 0
        for item_function, item_filename, item_delta in s_log_levels_items:
            if item_function and not function.startswith( item_function):
                continue
            if item_filename and not filename.startswith( item_filename):
                continue
            delta = item_delta
            break

        s_log_levels_cache[ key] = delta

    return delta


def log_levels_add( delta, filename_prefix, function_prefix):
    '''
    log() calls from locations with filenames starting with 
    and/or function names starting with  will have 
    added to their level.

    Use -ve delta to increase verbosity from particular filename or function
    prefixes.
    '''
    log( 'adding level: {filename_prefix=!r} {function_prefix=!r}')

    # Sort in reverse order so that long functions and filename specs come
    # first.
    #
    s_log_levels_items.append( (function_prefix, filename_prefix, delta))
    s_log_levels_items.sort( reverse=True)


def log( text, level=0, caller=1, nv=True, out=None):
    '''
    Writes log text, with special handling of {} items in 
    similar to python3's f-strings.

    text:
        The text to output.
    caller:
        How many frames to step up to get caller's context when evaluating
        file:line information and/or expressions. Or frame record as returned
        by inspect.stack()[].
    nv:
        If true, we expand {...} in  using expand_nv().
    out:
        Where to send output. If None we use sys.stdout.

     is evaluated in our caller's context ( stack frames up)
    using eval(), and expanded to  or =.

    If  ends with '=', this character is removed and we prefix the
    result with =.

    E.g.:
        x = 45
        y = 'hello'
        expand_nv( 'foo {x} {y=}')
    returns:
        foo 45 y=hello

     can also use ':' and '!' to control formatting, like
    str.format().
    '''
    if out is None:
        out = sys.stdout
    level += g_log_delta
    if isinstance( caller, int):
        caller += 1
    level += log_levels_find( caller)
    if level <= 0:
        text = log_text( text, caller, nv=nv)
        out.write( text)
        out.flush()


def log0( text, caller=1, nv=True, out=None):
    '''
    Most verbose log. Same as log().
    '''
    log( text, level=0, caller=caller+1, nv=nv, out=out)

def log1( text, caller=1, nv=True, out=None):
    log( text, level=1, caller=caller+1, nv=nv, out=out)

def log2( text, caller=1, nv=True, out=None):
    log( text, level=2, caller=caller+1, nv=nv, out=out)

def log3( text, caller=1, nv=True, out=None):
    log( text, level=3, caller=caller+1, nv=nv, out=out)

def log4( text, caller=1, nv=True, out=None):
    log( text, level=4, caller=caller+1, nv=nv, out=out)

def log5( text, caller=1, nv=True, out=None):
    '''
    Least verbose log.
    '''
    log( text, level=5, caller=caller+1, nv=nv, out=out)

def logx( text, caller=1, nv=True, out=None):
    '''
    Does nothing, useful when commenting out a log().
    '''
    pass


def log_levels_add_env( name='JLIB_log_levels'):
    '''
    Added log levels encoded in an environmental variable.
    '''
    t = os.environ.get( name)
    if t:
        for ffll in t.split( ','):
            ffl, delta = ffll.split( '=', 1)
            delta = int( delta)
            ffl = ffl.split( ':')
            if 0:
                pass
            elif len( ffl) == 1:
                filename = ffl
                function = None
            elif len( ffl) == 2:
                filename, function = ffl
            else:
                assert 0
            log_levels_add( delta, filename, function)


def strpbrk( text, substrings):
    '''
    Finds first occurrence of any item in  in .

    Returns (pos, substring) or (len(text), None) if not found.
    '''
    ret_pos = len( text)
    ret_substring = None
    for substring in substrings:
        pos = text.find( substring)
        if pos >= 0 and pos < ret_pos:
            ret_pos = pos
            ret_substring = substring
    return ret_pos, ret_substring


def split_first_of( text, substrings):
    '''
    Returns (pre, post), where 
 doesn't contain any item in 
    and  is empty or starts with an item in .
    '''
    pos, _ = strpbrk( text, substrings)
    return text[ :pos], text[ pos:]



log_levels_add_env()


def force_line_buffering():
    '''
    Ensure sys.stdout and sys.stderr are line-buffered. E.g. makes things work
    better if output is piped to a file via 'tee'.

    Returns original out,err streams.
    '''
    stdout0 = sys.stdout
    stderr0 = sys.stderr
    sys.stdout = os.fdopen( os.dup( sys.stdout.fileno()), 'w', 1)
    sys.stderr = os.fdopen( os.dup( sys.stderr.fileno()), 'w', 1)
    return stdout0, stderr0


def exception_info( exception=None, limit=None, out=None, prefix='', oneline=False):
    '''
    General replacement for traceback.* functions that print/return information
    about exceptions. This function provides a simple way of getting the
    functionality provided by these traceback functions:

        traceback.format_exc()
        traceback.format_exception()
        traceback.print_exc()
        traceback.print_exception()

    Returns:
        A string containing description of specified exception and backtrace.

    Inclusion of outer frames:
        We improve upon traceback.* in that we also include stack frames above
        the point at which an exception was caught - frames from the top-level
         or thread creation fn to the try..catch block, which makes
        backtraces much more useful.

        Google 'sys.exc_info backtrace incomplete' for more details.

        We deliberately leave a slightly curious pair of items in the backtrace
        - the point in the try: block that ended up raising an exception, and
        the point in the associated except: block from which we were called.

        For clarity, we insert an empty frame in-between these two items, so
        that one can easily distinguish the two parts of the backtrace.

        So the backtrace looks like this:

            root (e.g.  or /usr/lib/python2.7/threading.py:778:__bootstrap():
            ...
            file:line in the except: block where the exception was caught.
            ::(): marker
            file:line in the try: block.
            ...
            file:line where the exception was raised.

        The items after the ::(): marker are the usual items that traceback.*
        shows for an exception.

    Also the backtraces that are generated are more concise than those provided
    by traceback.* - just one line per frame instead of two - and filenames are
    output relative to the current directory if applicatble. And one can easily
    prefix all lines with a specified string, e.g. to indent the text.

    Returns a string containing backtrace and exception information, and sends
    returned string to  if specified.

    exception:
        None, or a (type, value, traceback) tuple, e.g. from sys.exc_info(). If
        None, we call sys.exc_info() and use its return value.
    limit:
        None or maximum number of stackframes to output.
    out:
        None or callable taking single  parameter or object with a
        'write' member that takes a single  parameter.
    prefix:
        Used to prefix all lines of text.
    '''
    if exception is None:
        exception = sys.exc_info()
    etype, value, tb = exception

    if sys.version_info[0] == 2:
        out2 = io.BytesIO()
    else:
        out2 = io.StringIO()
    try:

        frames = []

        # Get frames above point at which exception was caught - frames
        # starting at top-level  or thread creation fn, and ending
        # at the point in the catch: block from which we were called.
        #
        # These frames are not included explicitly in sys.exc_info()[2] and are
        # also omitted by traceback.* functions, which makes for incomplete
        # backtraces that miss much useful information.
        #
        for f in reversed(inspect.getouterframes(tb.tb_frame)):
            ff = f[1], f[2], f[3], f[4][0].strip()
            frames.append(ff)

        if 1:
            # It's useful to see boundary between upper and lower frames.
            frames.append( None)

        # Append frames from point in the try: block that caused the exception
        # to be raised, to the point at which the exception was thrown.
        #
        # [One can get similar information using traceback.extract_tb(tb):
        #   for f in traceback.extract_tb(tb):
        #       frames.append(f)
        # ]
        for f in inspect.getinnerframes(tb):
            ff = f[1], f[2], f[3], f[4][0].strip()
            frames.append(ff)

        cwd = os.getcwd() + os.sep
        if oneline:
            if etype and value:
                # The 'exception_text' variable below will usually be assigned
                # something like ': ', unless
                # there was no explanatory text provided (e.g. "raise Exception()").
                # In this case, str(value) will evaluate to ''.
                exception_text = traceback.format_exception_only(etype, value)[0].strip()
                filename, line, fnname, text = frames[-1]
                if filename.startswith(cwd):
                    filename = filename[len(cwd):]
                if not str(value):
                    # The exception doesn't have any useful explanatory text
                    # (for example, maybe it was raised by an expression like
                    # "assert " without a subsequent comma).  In
                    # the absence of anything more helpful, print the code that
                    # raised the exception.
                    exception_text += ' (%s)' % text
                line = '%s%s at %s:%s:%s()' % (prefix, exception_text, filename, line, fnname)
                out2.write(line)
        else:
            out2.write( '%sBacktrace:\n' % prefix)
            for frame in frames:
                if frame is None:
                    out2.write( '%s    ^except raise:\n' % prefix)
                    continue
                filename, line, fnname, text = frame
                if filename.startswith( cwd):
                    filename = filename[ len(cwd):]
                if filename.startswith( './'):
                    filename = filename[ 2:]
                out2.write( '%s    %s:%s:%s(): %s\n' % (
                        prefix, filename, line, fnname, text))

            if etype and value:
                out2.write( '%sException:\n' % prefix)
                lines = traceback.format_exception_only( etype, value)
                for line in lines:
                    out2.write( '%s    %s' % ( prefix, line))

        text = out2.getvalue()

        # Write text to  if specified.
        out = getattr( out, 'write', out)
        if callable( out):
            out( text)
        return text

    finally:
        # clear things to avoid cycles.
        exception = None
        etype = None
        value = None
        tb = None
        frames = None


def number_sep( s):
    '''
    Simple number formatter, adds commas in-between thousands.  can
    be a number or a string. Returns a string.
    '''
    if not isinstance( s, str):
        s = str( s)
    c = s.find( '.')
    if c==-1:   c = len(s)
    end = s.find('e')
    if end == -1:   end = s.find('E')
    if end == -1:   end = len(s)
    ret = ''
    for i in range( end):
        ret += s[i]
        if ic and i interval or override:
        debug_periodic_t0[0] = t
        debug(text)


def time_duration( seconds, verbose=False, s_format='%i'):
    '''
    Returns string expressing an interval.

    seconds:
        The duration in seconds
    verbose:
        If true, return like '4 days 1 hour 2 mins 23 secs', otherwise as
        '4d3h2m23s'.
    s_format:
        If specified, use as printf-style format string for seconds.
    '''
    x = abs(seconds)
    ret = ''
    i = 0
    for div, text in [
            ( 60, 'sec'),
            ( 60, 'min'),
            ( 24, 'hour'),
            ( None, 'day'),
            ]:
        force = ( x == 0 and i == 0)
        if div:
            remainder = x % div
            x = int( x/div)
        else:
            remainder = x
        if not verbose:
            text = text[0]
        if remainder or force:
            if verbose and remainder > 1:
                # plural.
                text += 's'
            if verbose:
                text = ' %s ' % text
            if i == 0:
                remainder = s_format % remainder
            ret = '%s%s%s' % ( remainder, text, ret)
        i += 1
    ret = ret.strip()
    if ret == '':
        ret = '0s'
    if seconds < 0:
        ret = '-%s' % ret
    return ret

assert time_duration( 303333) == '3d12h15m33s'
assert time_duration( 303333.33, s_format='%.1f') == '3d12h15m33.3s'
assert time_duration( 303333, verbose=True) == '3 days 12 hours 15 mins 33 secs'
assert time_duration( 303333.33, verbose=True, s_format='%.1f') == '3 days 12 hours 15 mins 33.3 secs'

assert time_duration( 0) == '0s'
assert time_duration( 0, verbose=True) == '0 sec'


def date_time( t=None):
    if t is None:
        t = time.time()
    return time.strftime( "%F-%T", time.gmtime( t))

def stream_prefix_time( stream):
    '''
    Returns StreamPrefix that prefixes lines with time and elapsed time.
    '''
    t_start = time.time()
    def prefix_time():
        return '%s (+%s): ' % (
                time.strftime( '%T'),
                time_duration( time.time() - t_start, s_format='0.1f'),
                )
    return StreamPrefix( stream, prefix_time)

def stdout_prefix_time():
    '''
    Changes sys.stdout to prefix time and elapsed time; returns original
    sys.stdout.
    '''
    ret = sys.stdout
    sys.stdout = stream_prefix_time( sys.stdout)
    return ret


def make_stream( out):
    '''
    If  already has a .write() member, returns .

    Otherwise a stream-like object with a .write() method that writes to .

    out:
        Where output is sent.
        If None, output is lost.
        Otherwise if an integer, we do: os.write( out, text)
        Otherwise if callable, we do: out( text)
        Otherwise we assume  is python stream or similar already.
    '''
    if getattr( out, 'write', None):
        return out
    class Ret:
        def flush():
            pass
    ret = Ret()
    if out is None:
        ret.write = lambda text: None
    elif isinstance( out, int):
        ret.write = lambda text: os.write( out, text)
    elif callable( out):
        ret.write = out
    else:
        ret.write = lambda text: out.write( text)
    return ret


def system_raw(
        command,
        out=None,
        shell=True,
        encoding='latin_1',
        errors='strict',
        buffer_len=-1,
        ):
    '''
    Runs command, writing output to  which can be an int fd, a python
    stream or a Stream object.

    Args:
        command:
            The command to run.
        out:
            Where output is sent.
            If None, output is lost.
            If -1, output is sent to stdout and stderr.
            Otherwise if an integer, we do: os.write( out, text)
            Otherwise if callable, we do: out( text)
            Otherwise we assume  is python stream or similar, and do: out.write(text)
        shell:
            Whether to run command inside a shell (see subprocess.Popen).
        encoding:
            Sepecify the encoding used to translate the command's output
            to characters.

            Note that if  is None and we are being run by python3,
             will be passed bytes, not a string.

            Note that latin_1 will never raise a UnicodeDecodeError.
        errors:
            How to handle encoding errors; see docs for codecs module for
            details.
        buffer_len:
            The number of bytes we attempt to read at a time. If -1 we read
            output one line at a time.

    Returns:
        subprocess's , i.e. -N means killed by signal N, otherwise
        the exit value (e.g. 12 if command terminated with exit(12)).
    '''
    if out == -1:
        stdin = 0
        stdout = 1
        stderr = 2
    else:
        stdin = None
        stdout = subprocess.PIPE
        stderr = subprocess.STDOUT
    child = subprocess.Popen(
            command,
            shell=shell,
            stdin=stdin,
            stdout=stdout,
            stderr=stderr,
            close_fds=True,
            #encoding=encoding - only python-3.6+.
            )

    child_out = child.stdout
    if encoding:
        child_out = codecs.getreader( encoding)( child_out, errors)

    out = make_stream( out)

    if stdout == subprocess.PIPE:
        if buffer_len == -1:
            for line in child_out:
                out.write( line)
        else:
            while 1:
                text = child_out.read( buffer_len)
                if not text:
                    break
                out.write( text)
    #decode( lambda : os.read( child_out.fileno(), 100), outfn, encoding)

    return child.wait()

if __name__ == '__main__':

    if os.getenv( 'jtest_py_system_raw_test') == '1':
        out = io.StringIO()
        system_raw(
                'jtest_py_system_raw_test=2 python jlib.py',
                sys.stdout,
                encoding='utf-8',
                #'latin_1',
                errors='replace',
                )
        print( repr( out.getvalue()))

    elif os.getenv( 'jtest_py_system_raw_test') == '2':
        for i in range(256):
            sys.stdout.write( chr(i))


def system(
        command,
        verbose=None,
        raise_errors=True,
        out=None,
        prefix=None,
        rusage=False,
        shell=True,
        encoding=None,
        errors='replace',
        buffer_len=-1,
        ):
    '''
    Runs a command like os.system() or subprocess.*, but with more flexibility.

    We give control over where the command's output is sent, whether to return
    the output and/or exit code, and whether to raise an exception if the
    command fails.

    We also support the use of /usr/bin/time to gather rusage information.

        command:
            The command to run.
        verbose:
            If true, we output information about the command that we run, and
            its result.

            If callable or something with a .write() method, information is
            sent to  itself. Otherwise it is sent to  (without
            applying ).
        raise_errors:
            If true, we raise an exception if the command fails, otherwise we
            return the failing error code or zero.
        out:
            Python stream, fd, callable or Stream instance to which output is
            sent.

            If  is 'return', we buffer the output and return (e,
            ). Note that if raise_errors is true, we only return if 
            is zero.

            If -1, output is sent to stdout and stderr.
        prefix:
            If not None, should be prefix string or callable used to prefix
            all output. [This is for convenience to avoid the need to do
            out=StreamPrefix(...).]
        rusage:
            If true, we run via /usr/bin/time and return rusage string
            containing information on execution.  and
            out='return' are ignored.
        shell:
            Passed to underlying subprocess.Popen() call.
        encoding:
            Sepecify the encoding used to translate the command's output
            to characters. Defaults to utf-8.
        errors:
            How to handle encoding errors; see docs for codecs module
            for details. Defaults to 'replace' so we never raise a
            UnicodeDecodeError.
        buffer_len:
            The number of bytes we attempt to read at a time. If -1 we read
            output one line at a time.

    Returns:
        If  is true, we return the rusage text.

        Else if raise_errors is true:
            If the command failed, we raise an exception.
            Else if  is 'return' we return the text output from the command.
            Else we return None

        Else if  is 'return', we return (e, text) where  is the
        command's exit code and  is the output from the command.

        Else we return , the command's exit code.
    '''
    if encoding is None:
        if sys.version_info[0] == 2:
            # python-2 doesn't seem to implement 'replace' properly.
            encoding = None
            errors = None
        else:
            encoding = 'utf-8'
            errors = 'replace'

    out_original = out
    if out is None:
        out = sys.stdout
    elif out == 'return':
        # Store the output ourselves so we can return it.
        out = io.StringIO()
    else:
        out = make_stream( out)

    if verbose:
        if getattr( verbose, 'write', None):
            pass
        elif callable( verbose):
            verbose = make_stream( verbose)
        else:
            verbose = out

    if prefix:
        out = StreamPrefix( out, prefix)

    if verbose:
        verbose.write( 'running: %s\n' % command)

    if rusage:
        command2 = ''
        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"'
        command2 += ' '
        command2 += command
        system_raw( command2, out, shell, encoding, errors, buffer_len=buffer_len)
        with open('ubt-out') as f:
            rusage_text = f.read()
        #print 'have read rusage output: %r' % rusage_text
        if rusage_text.startswith( 'Command '):
            # Annoyingly, /usr/bin/time appears to write 'Command
            # exited with ...' or 'Command terminated by ...' to the
            # output file before the rusage info if command doesn't
            # exit 0.
            nl = rusage_text.find('\n')
            rusage_text = rusage_text[ nl+1:]
        return rusage_text
    else:
        e = system_raw( command, out, shell, encoding, errors, buffer_len=buffer_len)

        if verbose:
            verbose.write( '[returned e=%s]\n' % e)

        if raise_errors:
            if e:
                raise Exception( 'command failed: %s' % command)
            if out_original == 'return':
                return out.getvalue()
            return

        if out_original == 'return':
            return e, out.getvalue()
        return e

def get_gitfiles( directory, submodules=False):
    '''
    Returns list of all files known to git in ;  must be
    somewhere within a git checkout.

    Returned names are all relative to .

    If .git directory, we also create /jtest-git-files. Otherwise we
    assume a this file already exists.
    '''
    if os.path.isdir( '%s/.git' % directory):
        command = 'cd ' + directory + ' && git ls-files'
        if submodules:
            command += ' --recurse-submodules'
        command += ' > jtest-git-files'
        system( command, verbose=sys.stdout)

    with open( '%s/jtest-git-files' % directory, 'r') as f:
        text = f.read()
    ret = text.split( '\n')
    return ret

def get_git_id_raw( directory):
    if not os.path.isdir( '%s/.git' % directory):
        return
    text = system(
            f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)',
            out='return',
            )
    return text

def get_git_id( directory, allow_none=False):
    '''
    Returns text where first line is ' ' and remaining
    lines contain output from 'git diff' in .

    directory:
        Root of git checkout.
    allow_none:
        If true, we return None if  is not a git checkout and
        jtest-git-id file does not exist.
    '''
    filename = f'{directory}/jtest-git-id'
    text = get_git_id_raw( directory)
    if text:
        with open( filename, 'w') as f:
            f.write( text)
    elif os.path.isfile( filename):
        with open( filename) as f:
            text = f.read()
    else:
        if not allow_none:
            raise Exception( f'Not in git checkout, and no file {filename}.')
        text = None
    return text

class Args:
    '''
    Iterates over argv items. Does getopt-style splitting of args starting with
    single '-' character.
    '''
    def __init__( self, argv):
        self.argv = argv
        self.pos = 0
        self.pos_sub = None
    def next( self):
        while 1:
            if self.pos >= len(self.argv):
                raise StopIteration()
            arg = self.argv[self.pos]
            if (not self.pos_sub
                    and arg.startswith('-')
                    and not arg.startswith('--')
                    ):
                # Start splitting current arg.
                self.pos_sub = 1
            if self.pos_sub and self.pos_sub >= len(arg):
                # End of '-' sub-arg.
                self.pos += 1
                self.pos_sub = None
                continue
            if self.pos_sub:
                # Return '-' sub-arg.
                ret = arg[self.pos_sub]
                self.pos_sub += 1
                return f'-{ret}'
            # Return normal arg.
            self.pos += 1
            return arg

def update_file( text, filename):
    '''
    Writes  to . Does nothing if contents of  are
    already .
    '''
    try:
        with open( filename) as f:
            text0 = f.read()
    except OSError:
        text0 = None
    if text == text0:
        log( 'Unchanged: ' + filename)
    else:
        log( 'Updating:  ' + filename)
        # Write to temp file and rename, to ensure we are atomic.
        filename_temp = f'{filename}-jlib-temp'
        with open( filename_temp, 'w') as f:
            f.write( text)
        os.rename( filename_temp, filename)


def mtime( filename, default=0):
    '''
    Returns mtime of file, or  if error - e.g. doesn't exist.
    '''
    try:
        return os.path.getmtime( filename)
    except OSError:
        return default

def get_filenames( paths):
    '''
    Yields each file in , walking any directories.
    '''
    if isinstance( paths, str):
        paths = (paths,)
    for name in paths:
        if os.path.isdir( name):
            for dirpath, dirnames, filenames in os.walk( name):
                for filename in filenames:
                    path = os.path.join( dirpath, filename)
                    yield path
        else:
            yield name

def remove( path):
    '''
    Removes file or directory, without raising exception if it doesn't exist.

    We assert-fail if the path still exists when we return, in case of
    permission problems etc.
    '''
    try:
        os.remove( path)
    except Exception:
        pass
    shutil.rmtree( path, ignore_errors=1)
    assert not os.path.exists( path)


# Things for figuring out whether files need updating, using mtimes.
#
def newest( names):
    '''
    Returns mtime of newest file in . Returns 0 if no file exists.
    '''
    assert isinstance( names, (list, tuple))
    assert names
    ret_t = 0
    ret_name = None
    for filename in get_filenames( names):
        t = mtime( filename)
        if t > ret_t:
            ret_t = t
            ret_name = filename
    return ret_t, ret_name

def oldest( names):
    '''
    Returns mtime of oldest file in  or 0 if no file exists.
    '''
    assert isinstance( names, (list, tuple))
    assert names
    ret_t = None
    ret_name = None
    for filename in get_filenames( names):
        t = mtime( filename)
        if ret_t is None or t < ret_t:
            ret_t = t
            ret_name = filename
    if ret_t is None:
        ret_t = 0
    return ret_t, ret_name

def update_needed( infiles, outfiles):
    '''
    If any file in  is newer than any file in , returns
    string description. Otherwise returns None.
    '''
    in_tmax, in_tmax_name = newest( infiles)
    out_tmin, out_tmin_name = oldest( outfiles)
    if in_tmax > out_tmin:
        text = f'{in_tmax_name} is newer than {out_tmin_name}'
        return text

def build(
        infiles,
        outfiles,
        command,
        force_rebuild=False,
        out=None,
        all_reasons=False,
        verbose=True,
        prefix=None,
        ):
    '''
    Ensures that  are up to date using enhanced makefile-like
    determinism of dependencies.

    Rebuilds  by running  if we determine that any of them
    are out of date.

    infiles:
        Names of files that are read by . Can be a single filename. If
        an item is a directory, we expand to all filenames in the directory's
        tree.
    outfiles:
        Names of files that are written by . Can also be a single
        filename.
    command:
        Command to run.
    force_rebuild:
        If true, we always re-run the command.
    out:
        A callable, passed to jlib.system(). If None, we use jlib.log() with
        our caller's stack record.
    all_reasons:
        If true we check all ways for a build being needed, even if we already
        know a build is needed; this only affects the diagnostic that we
        output.
    verbose:
        Passed to jlib.system().
    prefix:
        Passed to jlib.system().

    We compare mtimes of  and , and we also detect changes
    to the command itself.

    If any of infiles are newer than any of outfiles, or  is
    different to contents of commandfile '.cmd, then truncates
    commandfile and runs . If  succeeds we writes 
    to commandfile.
    '''
    if isinstance( infiles, str):
        infiles = (infiles,)
    if isinstance( outfiles, str):
        infiles = (outfiles,)

    if not out:
        out_frame_record = inspect.stack()[1]
        out = lambda text: log( text, nv=0, caller=out_frame_record)

    command_filename = f'{outfiles[0]}.cmd'

    reasons = []

    if not reasons or all_reasons:
        if force_rebuild:
            reasons.append( 'force_rebuild was specified')

    if not reasons or all_reasons:
        try:
            with open( command_filename) as f:
                command0 = f.read()
        except Exception:
            command0 = None
        if command != command0:
            if command0:
                reasons.append( 'command has changed')
            else:
                reasons.append( 'no previous command')

    if not reasons or all_reasons:
        reason = update_needed( infiles, outfiles)
        if reason:
            reasons.append( reason)

    if not reasons:
        out( 'Already up to date: ' + ' '.join(outfiles))
        return

    if out:
        out( 'Rebuilding because %s: %s' % (
                ', and '.join( reasons),
                ' '.join(outfiles),
                ))

    # Empty . For each unique item we
    use -L with parent directory, and -l with embedded name (without leading
    'lib' or trailing '.co').
    '''
    dirs = set()
    names = []
    if isinstance( sos, str):
        sos = (sos,)
    for so in sos:
        dir_ = os.path.dirname( so)
        name = os.path.basename( so)
        assert name.startswith( 'lib')
        assert name.endswith ( '.so')
        name = name[3:-3]
        dirs.add( dir_)
        names.append( name)
    ret = ''
    # Important to use sorted() here, otherwise ordering from set() is
    # arbitrary causing occasional spurious rebuilds by jlib.build().
    for dir_ in sorted(dirs):
        ret += f' -L {dir_}'
    for name in names:
        ret += f' -l {name}'
    return ret