def which(program):
    '''return None or the full path to an executable (respecting $PATH)
http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python/377028#377028
    '''
    fpath, fname = os.path.split(program)
    if fpath and isexecutable(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            path = path.strip('"')
            sut = os.path.join(path, program)
            if isexecutable(sut):
                return sut
    return None def firstfoundfile(locations):
    '''return the first existing file from a list of filenames (or None)'''
    for option in locations:
        if isreadablefile(option):
            return option
    return None def arg_isexecutable(f):
    if isexecutable(f):
        return f
    return None def isexecutable(f):
    '''True if argument is executable'''
    return os.path.isfile(f) and os.access(f, os.X_OK) def arg_isreadablefile(f):
    if isreadablefile(f):
        return f
    return None def isreadablefile(f):
    '''True if argument is readable file'''
    return os.path.isfile(f) and os.access(f, os.R_OK) def arg_isstr(s):
    if isstr(s):
        return s
    return None def isstr(s):
    '''True if argument is stringy (unicode or string)'''
    try:
        unicode
        stringy = (str, unicode)
    except NameError:
        stringy = (str,)  # -- python3
    return isinstance(s, stringy) class BaseDoctype(object):

    def __repr__(self):
        return '<%s:%s>' % (self.__class__.__name__, self.source.stem,)

    def __init__(self, *args, **kwargs):
        self.source = kwargs.get('source', None)
        self.output = kwargs.get('output', None)
        self.config = kwargs.get('config', None)
        self.removals = set()
        assert self.source is not None
        assert self.output is not None
        assert self.config is not None

    def cleanup(self):
        stem = self.source.stem
        removals = getattr(self, 'removals', None)
        if removals:
            for fn in removals:
                logger.debug("%s cleaning up intermediate file %s", stem, fn)
                try:
                    os.unlink(fn)
                except OSError as e:
                    if e.errno is errno.ENOENT:
                        logger.error("%s missing file at cleanup %s", stem, fn)
                    else:
                        raise e

    def build_precheck(self):
        classname = self.__class__.__name__
        if self.config.script:
            return True
        for tool, validator in self.required.items():
            thing = getattr(self.config, tool, None)
            logger.debug("%s, tool = %s, thing = %s", classname, tool, thing)
            if thing is None:
                logger.error("%s missing required tool %s, skipping...",
                             classname, tool)
                return False
            assert validator(thing)
        return True

    def clear_output(self, **kwargs):
        '''remove the entire output directory

        This method must be --script aware.  The method execute_shellscript()
        generates scripts into the directory that would be removed.  Thus, the
        behaviour is different depending on --script mode or --build mode.
        '''
        logger.debug("%s removing dir   %s.",
                     self.output.stem, self.output.dirname)
        if self.config.script:
            s = 'test -d "{output.dirname}" && rm -rf -- "{output.dirname}"'
            return self.shellscript(s, **kwargs)
        if os.path.exists(self.output.dirname):
            shutil.rmtree(self.output.dirname)
        return True

    def mkdir_output(self, **kwargs):
        '''create a new output directory

        This method must be --script aware.  The method execute_shellscript()
        generates scripts into the directory that would be removed.  Thus, the
        behaviour is different depending on --script mode or --build mode.
        '''
        logger.debug("%s creating dir   %s.",
                     self.output.stem, self.output.dirname)
        if self.config.script:
            s = 'mkdir -p -- "{output.logdir}"'
            return self.shellscript(s, **kwargs)
        for d in (self.output.dirname, self.output.logdir):
            if not os.path.isdir(d):
                os.mkdir(d)
        return True

    def chdir_output(self, **kwargs):
        '''chdir to the output directory (or write the script that would)'''
        logger.debug("%s chdir to dir   %s.",
                     self.output.stem, self.output.dirname)
        if self.config.script:
            s = '''
# - - - - - {source.stem} - - - - - -

cd -- "{output.dirname}"'''
            return self.shellscript(s, **kwargs)
        os.chdir(self.output.dirname)
        return True

    def generate_md5sums(self, **kwargs):
        logger.debug("%s generating MD5SUMS in %s.",
                     self.output.stem, self.output.dirname)
        timestr = time.strftime('%F-%T', time.gmtime())
        md5file = self.output.MD5SUMS
        if self.config.script:
            l = list()
            for fname, hashval in sorted(self.source.md5sums.items()):
                l.append('# {}  {}'.format(hashval, fname))
            md5s = '\n'.join(l)
            s = '''# -- MD5SUMS file from source tree at {}
#
# md5sum > {} -- {}
#
{}
#'''
            s = s.format(timestr,
                         md5file,
                         ' '.join(self.source.md5sums.keys()),
                         md5s)
            return self.shellscript(s, **kwargs)
        header = '# -- MD5SUMS for {}'.format(self.source.stem)
        writemd5sums(md5file, self.source.md5sums, header=header)
        return True

    def copy_static_resources(self, **kwargs):
        logger.debug("%s copy resources %s.",
                     self.output.stem, self.output.dirname)
        source = list()
        for d in self.config.resources:
            fullpath = os.path.join(self.source.dirname, d)
            fullpath = os.path.abspath(fullpath)
            if os.path.isdir(fullpath):
                source.append('"' + fullpath + '"')
        if not source:
            logger.debug("%s no images or resources to copy", self.source.stem)
            return True
        s = 'rsync --archive --verbose %s ./' % (' '.join(source))
        return self.shellscript(s, **kwargs)

    def hook_build_success(self):
        stem = self.output.stem
        logdir = self.output.logdir
        dirname = self.output.dirname
        logger.info("%s build SUCCESS  %s.", stem, dirname)
        logger.debug("%s removing logs  %s)", stem, logdir)
        if os.path.isdir(logdir):
            shutil.rmtree(logdir)
        return True

    def hook_build_failure(self):
        pass

    def shellscript(self, script, **kwargs):
        if self.config.build:
            return self.execute_shellscript(script, **kwargs)
        elif self.config.script:
            return self.dump_shellscript(script, **kwargs)
        else:
            etext = '%s in shellscript, neither --build nor --script'
            raise Exception(etext % (self.source.stem,))

    @logtimings(logger.debug)
    def dump_shellscript(self, script, preamble=preamble,
                         postamble=postamble, **kwargs):
        source = self.source
        output = self.output
        config = self.config
        file = kwargs.get('file', sys.stdout)
        s = script.format(output=output, source=source, config=config)
        print('', file=file)
        print(s, file=file)
        return True

    @logtimings(logger.debug)
    def execute_shellscript(self, script, preamble=preamble,
                            postamble=postamble, **kwargs):
        source = self.source
        output = self.output
        config = self.config

        logdir = output.logdir
        prefix = source.doctype.__name__ + '-'

        s = script.format(output=output, source=source, config=config)
        tf = ntf(dir=logdir, prefix=prefix, suffix='.sh', delete=False)
        tf.close()
        with codecs.open(tf.name, 'w', encoding='utf-8') as f:
            if preamble:
                f.write(preamble)
            f.write(s)
            if postamble:
                f.write(postamble)

        mode = stat.S_IXUSR | stat.S_IRUSR | stat.S_IWUSR
        os.chmod(tf.name, mode)

        cmd = [tf.name]
        result = execute(cmd, logdir=logdir)
        if result != 0:
            with codecs.open(tf.name, encoding='utf-8') as f:
                for line in f:
                    logger.info("Script: %s", line.rstrip())
            return False
        return True

    def build_prepare(self, **kwargs):
        stem = self.source.stem
        classname = self.__class__.__name__
        order = ['build_precheck',
                 'clear_output',
                 'mkdir_output',
                 'chdir_output',
                 'generate_md5sums',
                 'copy_static_resources',
                 ]

        for methname in order:
            method = getattr(self, methname, None)
            assert method is not None
            logger.info("%s calling method %s.%s",
                        stem, classname, method.__name__)
            if not method(**kwargs):
                logger.error("%s called method  %s.%s failed, skipping...",
                             stem, classname, method.__name__)
                return False
        return True

    def determinebuildorder(self):
        graph = nx.DiGraph()
        d = dict(inspect.getmembers(self, inspect.ismethod))
        for name, member in d.items():
            predecessors = getattr(member, 'depends', None)
            if predecessors:
                for pred in predecessors:
                    method = d.get(pred, None)
                    assert method is not None
                    graph.add_edge(method, member)
        order = nx.dag.topological_sort(graph)
        return order

    @logtimings(logger.debug)
    def build_fullrun(self, **kwargs):
        stem = self.source.stem
        order = self.determinebuildorder()
        logger.debug("%s build order %r", self.source.stem, order)
        for method in order:
            classname = self.__class__.__name__
            logger.info("%s calling method %s.%s",
                        stem, classname, method.__name__)
            if not method(**kwargs):
                logger.error("%s called method  %s.%s failed, skipping...",
                             stem, classname, method.__name__)
                return False
        return True

    @logtimings(logger.info)
    def generate(self, **kwargs):
        # -- perform build preparation steps;
        #     - check for all executables and data files
        #     - clear output dir
        #     - make output dir
        #     - chdir to output dir
        #     - copy source images/resources to output dir
        #
        if not self.config.script:
            opwd = os.getcwd()
        if not self.build_prepare():
            return False

        # -- build
        #
        result = self.build_fullrun(**kwargs)

        # -- always clean the kitchen
        #
        self.cleanup()

        # -- report on result and/or cleanup
        #
        if result:
            self.hook_build_success()
        else:
            self.hook_build_failure()

        if not self.config.script:
            os.chdir(opwd)

        return result class SignatureChecker(object):

    @classmethod
    def signatureLocation(cls, buf, fname):
        for sig in cls.signatures:
            try:
                sindex = buf.index(sig)
                logger.debug("YES FOUND signature %r in %s at %s; doctype %s.",
                             sig, fname, sindex, cls)
                return sindex
            except ValueError:
                logger.debug("not found signature %r in %s for type %s",
                             sig, fname, cls.__name__)
        return None def depends(*predecessors):
    '''decorator to be used for constructing build order graph'''
    def anon(f):
        @wraps(f)
        def method(self, *args, **kwargs):
            return f(self, *args, **kwargs)
        method.depends = [x.__name__ for x in predecessors]
        return method
    return anon
import logging
from tldp.utils import which, firstfoundfile
from tldp.utils import arg_isexecutable, isexecutable
from tldp.utils import arg_isreadablefile, isreadablefile
from tldp.utils import arg_isstr, isstr
from tldp.doctypes.common import BaseDoctype, SignatureChecker, depends
#! /usr/bin/python
# -*- coding: utf8 -*-
#
# Copyright (c) 2016 Linux Documentation Project

from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals




logger = logging.getLogger(__name__)


def xslchunk_finder():
    l = ['/usr/share/xml/docbook/stylesheet/ldp/html/tldp-sections.xsl',
         'http://docbook.sourceforge.net/release/xsl/current/html/chunk.xsl',
         ]
    return firstfoundfile(l)


def xslsingle_finder():
    l = ['/usr/share/xml/docbook/stylesheet/ldp/html/tldp-one-page.xsl',
         'http://docbook.sourceforge.net/release/xsl/current/html/docbook.xsl',
         ]
    return firstfoundfile(l)


def xslprint_finder():
    l = ['http://docbook.sourceforge.net/release/xsl/current/fo/docbook.xsl',
         # '/usr/share/xml/docbook/stylesheet/ldp/fo/tldp-print.xsl',
         ]
    return l[0]
    # return firstfoundfile(l)


class Docbook4XML(BaseDoctype, SignatureChecker):
    formatname = 'DocBook XML 4.x'
    extensions = ['.xml']
    signatures = ['-//OASIS//DTD DocBook XML V4.1.2//EN',
                  '-//OASIS//DTD DocBook XML V4.2//EN',
                  '-//OASIS//DTD DocBook XML V4.2//EN',
                  '-//OASIS//DTD DocBook XML V4.4//EN',
                  '-//OASIS//DTD DocBook XML V4.5//EN', ]
    required = {'docbook4xml_xsltproc': isexecutable,
                'docbook4xml_html2text': isexecutable,
                'docbook4xml_dblatex': isexecutable,
                'docbook4xml_fop': isexecutable,
                'docbook4xml_xmllint': isexecutable,
                'docbook4xml_xslchunk': isreadablefile,
                'docbook4xml_xslsingle': isreadablefile,
                'docbook4xml_xslprint': isstr,
                }

    def make_validated_source(self, **kwargs):
        s = '''"{config.docbook4xml_xmllint}" > "{output.validsource}" \\
                  --nonet \\
                  --noent \\
                  --xinclude \\
                  --postvalid \\
                  "{source.filename}"'''
        return self.shellscript(s, **kwargs)

    @depends(make_validated_source)
    def make_name_htmls(self, **kwargs):
        '''create a single page HTML output'''
        s = '''"{config.docbook4xml_xsltproc}" > "{output.name_htmls}" \\
                  --nonet \\
                  --stringparam admon.graphics.path images/ \\
                  --stringparam base.dir . \\
                  "{config.docbook4xml_xslsingle}" \\
                  "{output.validsource}"'''
        return self.shellscript(s, **kwargs)

    @depends(make_name_htmls)
    def make_name_txt(self, **kwargs):
        '''create text output'''
        s = '''"{config.docbook4xml_html2text}" > "{output.name_txt}" \\
                  -style pretty \\
                  -nobs \\
                  "{output.name_htmls}"'''
        return self.shellscript(s, **kwargs)


    @depends(make_validated_source)