<filename>pman/pman/core.py
import imp
import fnmatch
import os
import shutil
import subprocess
import sys
import time
from collections import OrderedDict
import configparser


from . import toml
from . import creationutils


class PManException(Exception):
    pass


class NoConfigError(PManException):
    pass


class CouldNotFindPythonError(PManException):
    pass


class BuildError(PManException):
    pass


class FrozenEnvironmentError(PManException):
    def __init__(self):
        super().__init__("Operation not supported in frozen applications")


_CONFIG_DEFAULTS = OrderedDict([
    ('general', OrderedDict([
        ('name', 'Game'),
        ('renderer', 'none'),
        ('material_mode', 'legacy'),
        ('physics_engine', 'builtin'),
    ])),
    ('build', OrderedDict([
        ('asset_dir', 'assets/'),
        ('export_dir', '.built_assets/'),
        ('ignore_patterns', []),
        ('converters', ['native2bam']),
    ])),
    ('run', OrderedDict([
        ('main_file', 'main.py'),
        ('auto_build', True),
        ('auto_save', True),
    ])),
])

_USER_CONFIG_DEFAULTS = OrderedDict([
    ('python', OrderedDict([
        ('path', ''),
        ('in_venv', False),
    ])),
])


def _update_conf(config):
    if 'general' in config:
        if 'render_plugin' in config['general']:
            if '/' in config['general']['render_plugin']:
                # Convert from path to module
                renderplugin = config['general']['render_plugin']
                rppath = get_abs_path(config, renderplugin)
                maindir = os.path.dirname(get_abs_path(config, config['run']['main_file']))
                rppath = os.path.splitext(os.path.relpath(rppath, maindir))[0]
                module_parts = rppath.split(os.sep)
                modname = '.'.join(module_parts)
                config['general']['render_plugin'] = modname
            config['general']['renderer'] = config['general']['render_plugin']
            del config['general']['render_plugin']
        if 'converter_hooks' in config['general']:
            config['general']['converters'] = [
                'blend2bam' if i == 'pman.hooks.converter_blend_bam' else i
                for i in config['general']['converter_hooks']
            ]
            del config['general']['convert_hooks']


def _get_config(startdir, conf_name, defaults):
    try:
        if startdir is None:
            startdir = os.getcwd()
    except FileNotFoundError:
        # The project folder was deleted on us
        raise NoConfigError("Could not find config file")

    dirs = os.path.abspath(startdir).split(os.sep)

    while dirs:
        cdir = os.sep.join(dirs)
        if cdir.strip() and conf_name in os.listdir(cdir):
            configpath = os.path.join(cdir, conf_name)

            confdict = toml.load(configpath)
            confdict = {
                k: dict(defaults.get(k, {}), **confdict.get(k, {}))
                for k in set(defaults.keys()) | set(confdict.keys())
            }

            confdict['internal'] = {
                'projectdir': os.path.dirname(configpath),
            }

            _update_conf(confdict)

            return confdict

        dirs.pop()

    # No config found
    raise NoConfigError("Could not find config file")


def get_config(startdir=None):
    return _get_config(startdir, '.pman', _CONFIG_DEFAULTS)


def config_exists(startdir=None):
    try:
        get_config(startdir)
        have_config = True
    except NoConfigError:
        have_config = False

    return have_config


def get_user_config(startdir=None):
    try:
        conf = _get_config(startdir, '.pman.user', _USER_CONFIG_DEFAULTS)
    except NoConfigError:
        # No user config, just create one
        config = get_config(startdir)
        file_path = os.path.join(config['internal']['projectdir'], '.pman.user')
        print("Creating user config at {}".format(file_path))
        open(file_path, 'w').close()

        conf = _get_config(startdir, '.pman.user', _USER_CONFIG_DEFAULTS)

    confpy = conf['python']['path']
    if not confpy:
        # Try to find a Python program to default to
        try:
            pyprog = get_python_program()
            pyloc = shutil.which(pyprog)
            conf['python']['path'] = pyloc

            activate_this_loc = os.path.join(os.path.dirname(pyloc), 'activate_this.py')
            conf['python']['in_venv'] = os.path.exists(activate_this_loc)

            write_user_config(conf)
        except CouldNotFindPythonError:
            pass

    return conf


def _write_config(config, conf_name):
    writecfg = config.copy()
    del writecfg['internal']

    with open(os.path.join(config['internal']['projectdir'], conf_name), 'w') as f:
        toml.dump(writecfg, f)


def write_config(config):
    _write_config(config, '.pman')


def write_user_config(user_config):
    _write_config(user_config, '.pman.user')


def is_frozen():
    return imp.is_frozen(__name__)


def create_project(projectdir='.', extras=None):
    if is_frozen():
        raise FrozenEnvironmentError()

    if not os.path.exists(projectdir):
        os.makedirs(projectdir)

    confpath = os.path.join(projectdir, '.pman')
    if os.path.exists(confpath):
        print("Updating project in {}".format(projectdir))
    else:
        print("Creating new project in {}".format(projectdir))

    if not os.path.exists(confpath):
        # Touch config file to make sure it is present
        with open(confpath, 'a') as _:
            pass

    config = get_config(projectdir)
    write_config(config)
    user_config = get_user_config(projectdir)

    creationutils.create_dirs(projectdir, (
        config['build']['asset_dir'],
        'tests',
    ))


    templatedir = creationutils.get_template_dir()
    creationutils.copy_template_files(projectdir, templatedir, (
        ('main.py', config['run']['main_file']),
        ('settings.prc', 'settings.prc'),
        ('requirements.txt', 'requirements.txt'),
        ('setup.py', 'setup.py'),
        ('setup.cfg', 'setup.cfg'),
        ('pylintrc', '.pylintrc'),
        ('test_imports.py', 'tests/test_imports.py'),
    ))

    if extras:
        import pkg_resources
        entrypoints = {
            entrypoint.name: entrypoint.load()
            for entrypoint in pkg_resources.iter_entry_points('pman.creation_extras')
        }
        for extra in extras:
            if extra not in entrypoints:
                print('Could not find creation extra: {}'.format(extra))
                continue
            entrypoints[extra](projectdir, config, user_config)



def get_abs_path(config, path):
    return PMan(config=config).get_abs_path(path)


def get_rel_path(config, path):
    return PMan(config=config).get_rel_path(path)

def get_python_program(config=None):
    python_programs = [
        'ppython',
        'python3',
        'python',
        'python2',
    ]

    if config is not None:
        user_config = get_user_config(config['internal']['projectdir'])
        confpy = user_config['python']['path']
        if confpy:
            python_programs.insert(0, confpy)

    # Check to see if there is a version of Python that can import panda3d
    for pyprog in python_programs:
        args = [
            pyprog,
            '-c',
            'import panda3d.core; import direct',
        ]
        with open(os.devnull, 'w') as f:
            try:
                retcode = subprocess.call(args, stderr=f)
            except FileNotFoundError:
                retcode = 1

        if retcode == 0:
            return pyprog

    # We couldn't find a python program to run
    raise CouldNotFindPythonError('Could not find a Python version with Panda3D installed')


def in_venv():
    return (
        hasattr(sys, 'real_prefix') or
        (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
    )


def run_program(config, args, use_venv=True, cwd=None):
    user_config = get_user_config(config['internal']['projectdir'])
    if use_venv and user_config['python']['in_venv']:
        actv_this_loc = os.path.join(
            os.path.dirname(user_config['python']['path']),
            'activate_this.py'
        )
        args = [
            'python',
            os.path.join(os.path.dirname(__file__), 'venvwrapper.py'),
            actv_this_loc,
        ] + args
    subprocess.call(args, cwd=cwd)

def run_script(config, args, use_venv=True, cwd=None):
    user_config = get_user_config(config['internal']['projectdir'])
    if use_venv and user_config['python']['in_venv']:
        pyprog = 'python'
    else:
        pyprog = get_python_program(config)
    run_program(config, [pyprog] + args, use_venv=use_venv, cwd=cwd)


def build(config=None):
    PMan(config=config).build()


def run(config=None):
    PMan(config=config).run()


def dist(config=None, build_installers=True, platforms=None):
    PMan(config=config).dist(build_installers, platforms)


def clean(config=None):
    PMan(config=config).clean()


def create_renderer(base, config=None):
    if not is_frozen():
        if config is None:
            config = get_config()
        sys.path.append(get_abs_path(config, config['build']['export_dir']))
    import pman_renderer #pylint: disable=import-error
    return pman_renderer.get_renderer()(base)


_RENDER_STUB = """
import functools
def get_renderer():
    modname = '{}'
    attrs = {}
    module =  __import__(modname, fromlist=['__name__'], level=0)
    return functools.reduce(getattr, attrs, module)
"""


RENDER_STUB_NAME = 'pman_renderer.py'


def converter_copy(_config, _user_config, srcdir, dstdir, assets):
    for asset in assets:
        src = asset
        dst = src.replace(srcdir, dstdir)
        # print('Copying file from "{}" to "{}"'.format(src, dst))
        if not os.path.exists(os.path.dirname(dst)):
            os.makedirs(os.path.dirname(dst))
        shutil.copyfile(src, dst)


class PMan(object):
    def __init__(self, config=None, config_startdir=None):
        if config:
            self.config = config
            self.user_config = get_user_config(config['internal']['projectdir'])
        else:
            self.config = get_config(config_startdir)
            self.user_config = get_user_config(config_startdir)

        if is_frozen():
            self.converters = []
        else:
            import pkg_resources
            self.converters = [
                entry_point.load()
                for entry_point in pkg_resources.iter_entry_points('pman.converters')
                if entry_point.name in self.config['build']['converters']
            ]

    def get_abs_path(self, path):
        return os.path.join(
            self.config['internal']['projectdir'],
            path
        )

    def get_rel_path(self, path):
        return os.path.relpath(path, self.config['internal']['projectdir'])

    def build(self):
        import pkg_resources
        if is_frozen():
            raise FrozenEnvironmentError()

        if hasattr(time, 'perf_counter'):
            #pylint:disable=no-member
            stime = time.perf_counter()
        else:
            stime = time.time()
        print("Starting build")

        srcdir = self.get_abs_path(self.config['build']['asset_dir'])
        dstdir = self.get_abs_path(self.config['build']['export_dir'])

        if not os.path.exists(srcdir):
            print("warning: could not find asset directory: {}".format(srcdir))
            return

        if not os.path.exists(dstdir):
            print("Creating asset export directory at {}".format(dstdir))
            os.makedirs(dstdir)

        print("Read assets from: {}".format(srcdir))
        print("Export them to: {}".format(dstdir))

        ignore_patterns = self.config['build']['ignore_patterns']
        print("Ignoring file patterns: {}".format(ignore_patterns))

        # Gather files and group by extension
        ext_asset_map = {}
        ext_dst_map = {}
        ext_converter_map = {}
        for converter in self.converters:
            ext_dst_map.update(converter.ext_dst_map)
            for ext in converter.supported_exts:
                ext_converter_map[ext] = converter

        for root, _dirs, files in os.walk(srcdir):
            for asset in files:
                src = os.path.join(root, asset)
                dst = src.replace(srcdir, dstdir)

                ignore_pattern = None
                for pattern in ignore_patterns:
                    if fnmatch.fnmatch(asset, pattern):
                        ignore_pattern = pattern
                        break
                if ignore_pattern is not None:
                    print('Skip building file {} that matched ignore pattern {}'.format(asset, ignore_pattern))
                    continue

                ext = '.' + asset.split('.', 1)[1]

                if ext in ext_dst_map:
                    dst = dst.replace(ext, ext_dst_map[ext])

                if os.path.exists(dst) and os.stat(src).st_mtime <= os.stat(dst).st_mtime:
                    print('Skip building up-to-date file: {}'.format(dst))
                    continue

                if ext not in ext_asset_map:
                    ext_asset_map[ext] = []

                print('Adding {} to conversion list to satisfy {}'.format(src, dst))
                ext_asset_map[ext].append(os.path.join(root, asset))

        # Find which extensions have hooks available
        convert_hooks = []
        for ext, converter in ext_converter_map.items():
            if ext in ext_asset_map:
                convert_hooks.append((converter, ext_asset_map[ext]))
                del ext_asset_map[ext]

        # Copy what is left
        for ext in ext_asset_map:
            converter_copy(self.config, self.user_config, srcdir, dstdir, ext_asset_map[ext])

        # Now run hooks that non-converted assets are in place (copied)
        for convert_hook in convert_hooks:
            convert_hook[0](self.config, self.user_config, srcdir, dstdir, convert_hook[1])

        # Write out stub importer so we do not need pkg_resources at runtime
        renderername = self.config['general']['renderer']

        if not renderername:
            renderername = _CONFIG_DEFAULTS['general']['renderer']
        for entry_point in pkg_resources.iter_entry_points('pman.renderers'):
            if entry_point.name == renderername:
                renderer_entry_point = entry_point
                break
        else:
            raise BuildError('Could not find renderer for {0}'.format(renderername))
        renderer_stub_path = os.path.join(dstdir, RENDER_STUB_NAME)
        print('Writing renderer stub to {}'.format(renderer_stub_path))
        with open(renderer_stub_path, 'w') as renderer_stub_file:
            renderer_stub_file.write(_RENDER_STUB.format(
                renderer_entry_point.module_name,
                repr(renderer_entry_point.attrs)
            ))


        if hasattr(time, 'perf_counter'):
            #pylint:disable=no-member
            etime = time.perf_counter()
        else:
            etime = time.time()
        print("Build took {:.4f}s".format(etime - stime))

    def run(self):
        if is_frozen():
            raise FrozenEnvironmentError()

        mainfile = self.get_abs_path(self.config['run']['main_file'])
        print("Running main file: {}".format(mainfile))
        args = [mainfile]
        #print("Args: {}".format(args))
        run_script(self.config, args, cwd=self.config['internal']['projectdir'])

    def dist(self, build_installers=True, platforms=None):
        if is_frozen():
            raise FrozenEnvironmentError()

        args = [
            'setup.py',
        ]

        if build_installers:
            args += ['bdist_apps']
        else:
            args += ['build_apps']

        if platforms is not None:
            args += ['-p', '{}'.format(','.join(platforms))]

        run_script(self.config, args, cwd=self.config['internal']['projectdir'])

    def clean(self):
        if is_frozen():
            raise FrozenEnvironmentError()

        shutil.rmtree(self.get_abs_path(self.config['build']['export_dir']), ignore_errors=True)
        shutil.rmtree(self.get_abs_path('build'), ignore_errors=True)
        shutil.rmtree(self.get_abs_path('dist'), ignore_errors=True)

<filename>pman/pman/core.py
import imp
import fnmatch
import os
import shutil
import subprocess
import sys
import time
from collections import OrderedDict
import configparser


from . import toml
from . import creationutils


class PManException(Exception):
    pass


class NoConfigError(PManException):
    pass


class CouldNotFindPythonError(PManException):
    pass


class BuildError(PManException):
    pass


class FrozenEnvironmentError(PManException):
    def __init__(self):
        super().__init__("Operation not supported in frozen applications")


_CONFIG_DEFAULTS = OrderedDict([
    ('general', OrderedDict([
        ('name', 'Game'),
        ('renderer', 'none'),
        ('material_mode', 'legacy'),
        ('physics_engine', 'builtin'),
    ])),
    ('build', OrderedDict([
        ('asset_dir', 'assets/'),
        ('export_dir', '.built_assets/'),
        ('ignore_patterns', []),
        ('converters', ['native2bam']),
    ])),
    ('run', OrderedDict([
        ('main_file', 'main.py'),
        ('auto_build', True),
        ('auto_save', True),
    ])),
])

_USER_CONFIG_DEFAULTS = OrderedDict([
    ('python', OrderedDict([
        ('path', ''),
        ('in_venv', False),
    ])),
])


def _update_conf(config):
    if 'general' in config:
        if 'render_plugin' in config['general']:
            if '/' in config['general']['render_plugin']:
                # Convert from path to module
                renderplugin = config['general']['render_plugin']
                rppath = get_abs_path(config, renderplugin)
                maindir = os.path.dirname(get_abs_path(config, config['run']['main_file']))
                rppath = os.path.splitext(os.path.relpath(rppath, maindir))[0]
                module_parts = rppath.split(os.sep)
                modname = '.'.join(module_parts)
                config['general']['render_plugin'] = modname
            config['general']['renderer'] = config['general']['render_plugin']
            del config['general']['render_plugin']
        if 'converter_hooks' in config['general']:
            config['general']['converters'] = [
                'blend2bam' if i == 'pman.hooks.converter_blend_bam' else i
                for i in config['general']['converter_hooks']
            ]
            del config['general']['convert_hooks']


def _get_config(startdir, conf_name, defaults):
    try:
        if startdir is None:
            startdir = os.getcwd()
    except FileNotFoundError:
        # The project folder was deleted on us
        raise NoConfigError("Could not find config file")

    dirs = os.path.abspath(startdir).split(os.sep)

    while dirs:
        cdir = os.sep.join(dirs)
        if cdir.strip() and conf_name in os.listdir(cdir):
            configpath = os.path.join(cdir, conf_name)

            confdict = toml.load(configpath)
            confdict = {
                k: dict(defaults.get(k, {}), **confdict.get(k, {}))
                for k in set(defaults.keys()) | set(confdict.keys())
            }

            confdict['internal'] = {
                'projectdir': os.path.dirname(configpath),
            }

            _update_conf(confdict)

            return confdict

        dirs.pop()

    # No config found
    raise NoConfigError("Could not find config file")


def get_config(startdir=None):
    return _get_config(startdir, '.pman', _CONFIG_DEFAULTS)


def config_exists(startdir=None):
    try:
        get_config(startdir)
        have_config = True
    except NoConfigError:
        have_config = False

    return have_config


def get_user_config(startdir=None):
    try:
        conf = _get_config(startdir, '.pman.user', _USER_CONFIG_DEFAULTS)
    except NoConfigError:
        # No user config, just create one
        config = get_config(startdir)
        file_path = os.path.join(config['internal']['projectdir'], '.pman.user')
        print("Creating user config at {}".format(file_path))
        open(file_path, 'w').close()

        conf = _get_config(startdir, '.pman.user', _USER_CONFIG_DEFAULTS)

    confpy = conf['python']['path']
    if not confpy:
        # Try to find a Python program to default to
        try:
            pyprog = get_python_program()
            pyloc = shutil.which(pyprog)
            conf['python']['path'] = pyloc

            activate_this_loc = os.path.join(os.path.dirname(pyloc), 'activate_this.py')
            conf['python']['in_venv'] = os.path.exists(activate_this_loc)

            write_user_config(conf)
        except CouldNotFindPythonError:
            pass

    return conf


def _write_config(config, conf_name):
    writecfg = config.copy()
    del writecfg['internal']

    with open(os.path.join(config['internal']['projectdir'], conf_name), 'w') as f:
        toml.dump(writecfg, f)


def write_config(config):
    _write_config(config, '.pman')


def write_user_config(user_config):
    _write_config(user_config, '.pman.user')


def is_frozen():
    return imp.is_frozen(__name__)


def create_project(projectdir='.', extras=None):
    if is_frozen():
        raise FrozenEnvironmentError()

    if not os.path.exists(projectdir):
        os.makedirs(projectdir)

    confpath = os.path.join(projectdir, '.pman')
    if os.path.exists(confpath):
        print("Updating project in {}".format(projectdir))
    else:
        print("Creating new project in {}".format(projectdir))

    if not os.path.exists(confpath):
        # Touch config file to make sure it is present
        with open(confpath, 'a') as _:
            pass

    config = get_config(projectdir)
    write_config(config)
    user_config = get_user_config(projectdir)

    creationutils.create_dirs(projectdir, (
        config['build']['asset_dir'],
        'tests',
    ))


    templatedir = creationutils.get_template_dir()
    creationutils.copy_template_files(projectdir, templatedir, (
        ('main.py', config['run']['main_file']),
        ('settings.prc', 'settings.prc'),
        ('requirements.txt', 'requirements.txt'),
        ('setup.py', 'setup.py'),
        ('setup.cfg', 'setup.cfg'),
        ('pylintrc', '.pylintrc'),
        ('test_imports.py', 'tests/test_imports.py'),
    ))

    if extras:
        import pkg_resources
        entrypoints = {
            entrypoint.name: entrypoint.load()
            for entrypoint in pkg_resources.iter_entry_points('pman.creation_extras')
        }
        for extra in extras:
            if extra not in entrypoints:
                print('Could not find creation extra: {}'.format(extra))
                continue
            entrypoints[extra](projectdir, config, user_config)



def get_abs_path(config, path):
    return PMan(config=config).get_abs_path(path)


def get_rel_path(config, path):
    return PMan(config=config).get_rel_path(path)

def get_python_program(config=None):
    python_programs = [
        'ppython',
        'python3',
        'python',
        'python2',
    ]

    if config is not None:
        user_config = get_user_config(config['internal']['projectdir'])
        confpy = user_config['python']['path']
        if confpy:
            python_programs.insert(0, confpy)

    # Check to see if there is a version of Python that can import panda3d
    for pyprog in python_programs:
        args = [
            pyprog,
            '-c',
            'import panda3d.core; import direct',
        ]
        with open(os.devnull, 'w') as f:
            try:
                retcode = subprocess.call(args, stderr=f)
            except FileNotFoundError:
                retcode = 1

        if retcode == 0:
            return pyprog

    # We couldn't find a python program to run
    raise CouldNotFindPythonError('Could not find a Python version with Panda3D installed')


def in_venv():
    return (
        hasattr(sys, 'real_prefix') or
        (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
    )


def run_program(config, args, use_venv=True, cwd=None):
    user_config = get_user_config(config['internal']['projectdir'])
    if use_venv and user_config['python']['in_venv']:
        actv_this_loc = os.path.join(
            os.path.dirname(user_config['python']['path']),
            'activate_this.py'
        )
        args = [
            'python',
            os.path.join(os.path.dirname(__file__), 'venvwrapper.py'),
            actv_this_loc,
        ] + args
    subprocess.call(args, cwd=cwd)

def run_script(config, args, use_venv=True, cwd=None):
    user_config = get_user_config(config['internal']['projectdir'])
    if use_venv and user_config['python']['in_venv']:
        pyprog = 'python'
    else:
        pyprog = get_python_program(config)
    run_program(config, [pyprog] + args, use_venv=use_venv, cwd=cwd)


def build(config=None):
    PMan(config=config).build()


def run(config=None):
    PMan(config=config).run()


def dist(config=None, build_installers=True, platforms=None):
    PMan(config=config).dist(build_installers, platforms)


def clean(config=None):
    PMan(config=config).clean()


def create_renderer(base, config=None):
    if not is_frozen():
        if config is None:
            config = get_config()
        sys.path.append(get_abs_path(config, config['build']['export_dir']))
    import pman_renderer #pylint: disable=import-error
    return pman_renderer.get_renderer()(base)


_RENDER_STUB = """
import functools
def get_renderer():
    modname = '{}'
    attrs = {}
    module =  __import__(modname, fromlist=['__name__'], level=0)
    return functools.reduce(getattr, attrs, module)
"""


RENDER_STUB_NAME = 'pman_renderer.py'


def converter_copy(_config, _user_config, srcdir, dstdir, assets):
    for asset in assets:
        src = asset
        dst = src.replace(srcdir, dstdir)
        # print('Copying file from "{}" to "{}"'.format(src, dst))
        if not os.path.exists(os.path.dirname(dst)):
            os.makedirs(os.path.dirname(dst))
        shutil.copyfile(src, dst)


class PMan(object):
    def __init__(self, config=None, config_startdir=None):
        if config:
            self.config = config
            self.user_config = get_user_config(config['internal']['projectdir'])
        else:
            self.config = get_config(config_startdir)
            self.user_config = get_user_config(config_startdir)

        if is_frozen():
            self.converters = []
        else:
            import pkg_resources
            self.converters = [
                entry_point.load()
                for entry_point in pkg_resources.iter_entry_points('pman.converters')
                if entry_point.name in self.config['build']['converters']
            ]

    def get_abs_path(self, path):
        return os.path.join(
            self.config['internal']['projectdir'],
            path
        )

    def get_rel_path(self, path):
        return os.path.relpath(path, self.config['internal']['projectdir'])

    def build(self):
        import pkg_resources
        if is_frozen():
            raise FrozenEnvironmentError()

        if hasattr(time, 'perf_counter'):
            #pylint:disable=no-member
            stime = time.perf_counter()
        else:
            stime = time.time()
        print("Starting build")

        srcdir = self.get_abs_path(self.config['build']['asset_dir'])
        dstdir = self.get_abs_path(self.config['build']['export_dir'])

        if not os.path.exists(srcdir):
            print("warning: could not find asset directory: {}".format(srcdir))
            return

        if not os.path.exists(dstdir):
            print("Creating asset export directory at {}".format(dstdir))
            os.makedirs(dstdir)

        print("Read assets from: {}".format(srcdir))
        print("Export them to: {}".format(dstdir))

        ignore_patterns = self.config['build']['ignore_patterns']
        print("Ignoring file patterns: {}".format(ignore_patterns))

        # Gather files and group by extension
        ext_asset_map = {}
        ext_dst_map = {}
        ext_converter_map = {}
        for converter in self.converters:
            ext_dst_map.update(converter.ext_dst_map)
            for ext in converter.supported_exts:
                ext_converter_map[ext] = converter

        for root, _dirs, files in os.walk(srcdir):
            for asset in files:
                src = os.path.join(root, asset)
                dst = src.replace(srcdir, dstdir)

                ignore_pattern = None
                for pattern in ignore_patterns:
                    if fnmatch.fnmatch(asset, pattern):
                        ignore_pattern = pattern
                        break
                if ignore_pattern is not None:
                    print('Skip building file {} that matched ignore pattern {}'.format(asset, ignore_pattern))
                    continue

                ext = '.' + asset.split('.', 1)[1]

                if ext in ext_dst_map:
                    dst = dst.replace(ext, ext_dst_map[ext])

                if os.path.exists(dst) and os.stat(src).st_mtime <= os.stat(dst).st_mtime:
                    print('Skip building up-to-date file: {}'.format(dst))
                    continue

                if ext not in ext_asset_map:
                    ext_asset_map[ext] = []

                print('Adding {} to conversion list to satisfy {}'.format(src, dst))
                ext_asset_map[ext].append(os.path.join(root, asset))

        # Find which extensions have hooks available
        convert_hooks = []
        for ext, converter in ext_converter_map.items():
            if ext in ext_asset_map:
                convert_hooks.append((converter, ext_asset_map[ext]))
                del ext_asset_map[ext]

        # Copy what is left
        for ext in ext_asset_map:
            converter_copy(self.config, self.user_config, srcdir, dstdir, ext_asset_map[ext])

        # Now run hooks that non-converted assets are in place (copied)
        for convert_hook in convert_hooks:
            convert_hook[0](self.config, self.user_config, srcdir, dstdir, convert_hook[1])

        # Write out stub importer so we do not need pkg_resources at runtime
        renderername = self.config['general']['renderer']

        if not renderername:
            renderername = _CONFIG_DEFAULTS['general']['renderer']
        for entry_point in pkg_resources.iter_entry_points('pman.renderers'):
            if entry_point.name == renderername:
                renderer_entry_point = entry_point
                break
        else:
            raise BuildError('Could not find renderer for {0}'.format(renderername))
        renderer_stub_path = os.path.join(dstdir, RENDER_STUB_NAME)
        print('Writing renderer stub to {}'.format(renderer_stub_path))
        with open(renderer_stub_path, 'w') as renderer_stub_file:
            renderer_stub_file.write(_RENDER_STUB.format(
                renderer_entry_point.module_name,
                repr(renderer_entry_point.attrs)
            ))


        if hasattr(time, 'perf_counter'):
            #pylint:disable=no-member
            etime = time.perf_counter()
        else:
            etime = time.time()
        print("Build took {:.4f}s".format(etime - stime))

    def run(self):
        if is_frozen():
            raise FrozenEnvironmentError()

        mainfile = self.get_abs_path(self.config['run']['main_file'])
        print("Running main file: {}".format(mainfile))
        args = [mainfile]
        #print("Args: {}".format(args))
        run_script(self.config, args, cwd=self.config['internal']['projectdir'])

    def dist(self, build_installers=True, platforms=None):
        if is_frozen():
            raise FrozenEnvironmentError()

        args = [
            'setup.py',
        ]

        if build_installers:
            args += ['bdist_apps']
        else:
            args += ['build_apps']

        if platforms is not None:
            args += ['-p', '{}'.format(','.join(platforms))]

        run_script(self.config, args, cwd=self.config['internal']['projectdir'])

    def clean(self):
        if is_frozen():
            raise FrozenEnvironmentError()

        shutil.rmtree(self.get_abs_path(self.config['build']['export_dir']), ignore_errors=True)
        shutil.rmtree(self.get_abs_path('build'), ignore_errors=True)
        shutil.rmtree(self.get_abs_path('dist'), ignore_errors=True)

# Convert from path to module # The project folder was deleted on us # No config found # No user config, just create one # Try to find a Python program to default to # Touch config file to make sure it is present # Check to see if there is a version of Python that can import panda3d # We couldn't find a python program to run #pylint: disable=import-error import functools def get_renderer():     modname = '{}'     attrs = {}     module =  __import__(modname, fromlist=['__name__'], level=0)     return functools.reduce(getattr, attrs, module) # print('Copying file from "{}" to "{}"'.format(src, dst)) #pylint:disable=no-member # Gather files and group by extension # Find which extensions have hooks available # Copy what is left # Now run hooks that non-converted assets are in place (copied) # Write out stub importer so we do not need pkg_resources at runtime #pylint:disable=no-member #print("Args: {}".format(args))