02cce6df创建于 2021年6月30日历史提交
import sys, os, glob, re, subprocess, shutil
import distutils.cmd, distutils.command.build_py
import setuptools
import setuptools.command.build_ext
from setuptools import setup, Extension
import sysconfig, subprocess, tempfile


class build_py(distutils.command.build_py.build_py):
    def run(self):
        try:
            config_path = os.path.join('subsync', 'config.py')
            config_path_template = os.path.join('subsync', 'config.py.template')

            if not os.path.isfile(config_path):
                print('copying {} -> {}'.format(config_path_template, config_path))
                shutil.copyfile(config_path_template, config_path)
        except:
            pass

        update_version = False
        try:
            version_long, version = read_version()
            update_version = version_long is not None

            import subsync
            update_version = subsync.version()[1] != version_long
        except:
            pass

        if update_version:
            write_version_file(version_long, version)

        super().run()


class build_ext(setuptools.command.build_ext.build_ext):
    user_options = setuptools.command.build_ext.build_ext.user_options + [
            ('use-pkg-config=', None, 'yes/no, use pkg-config to obtain information about installed libraries'),
            ('ffmpeg-dir=', None, 'ffmpeg library location'),
            ('sphinxbase-dir=', None, 'sphinxbase library location'),
            ('pocketsphinx-dir=', None, 'pocketsphinx library location'),
            ]

    def initialize_options(self):
        super().initialize_options()
        self.cflags  = []
        self.ldflags = []
        self.use_pkg_config   = os.environ.get('USE_PKG_CONFIG')
        self.ffmpeg_dir       = os.environ.get('FFMPEG_DIR')
        self.sphinxbase_dir   = os.environ.get('SPHINXBASE_DIR')
        self.pocketsphinx_dir = os.environ.get('POCKETSPHINX_DIR')

    def finalize_options(self):
        super().finalize_options()

        use_pkg_config = False
        if self.use_pkg_config is None:
            use_pkg_config = self.has_pkg_config()
        elif self.use_pkg_config == 'yes':
            use_pkg_config = True
            if not self.has_pkg_config():
                raise Exception('pkg-config not available')
        elif self.use_pkg_config == 'no':
            use_pkg_config = False
        else:
            raise Exception('--use-pkg-config invalid value {}, should be yes or no'.format(self.use_pkg_config))

        if self.ffmpeg_dir is not None and not os.path.isdir(self.ffmpeg_dir):
            raise Exception('ffmpeg directory does not exists: '.format(self.ffmpeg_dir))
        if self.sphinxbase_dir is not None and not os.path.isdir(self.sphinxbase_dir):
            raise Exception('sphinxbase directory does not exists: '.format(self.sphinxbase_dir))
        if self.pocketsphinx_dir is not None and not os.path.isdir(self.pocketsphinx_dir):
            raise Exception('pocketsphinx directory does not exists: '.format(self.pocketsphinx_dir))

        ffmpeg_libs = [
                'avdevice',
                'avformat',
                'avfilter',
                'avcodec',
                'swresample',
                'swscale',
                'avutil',
                ]

        sphinx_libs = [
                'pocketsphinx',
                'sphinxbase',
                ]

        if use_pkg_config:
            pkgs = [ 'lib' + name for name in ffmpeg_libs ] + sphinx_libs
            self.cflags       += self.get_pkg_config('--cflags-only-other', pkgs)
            self.ldflags      += self.get_pkg_config('--libs-only-other',   pkgs)
            self.include_dirs += self.get_pkg_config('--cflags-only-I', pkgs, strip_prefixes=['-I'])
            self.library_dirs += self.get_pkg_config('--libs-only-L', pkgs, strip_prefixes=['-L', '-R'])
            self.libraries    += self.get_pkg_config('--libs-only-l', pkgs, strip_prefixes=['-l'])

        else:
            self.libraries += ffmpeg_libs + sphinx_libs

        import pybind11
        self.include_dirs += self.get_paths(pybind11.get_include(), '')
        self.include_dirs += self.get_paths(pybind11.get_include(True), '')

        self.include_dirs += self.get_paths(self.ffmpeg_dir,       '', 'include')
        self.include_dirs += self.get_paths(self.sphinxbase_dir,   '', 'include')
        self.include_dirs += self.get_paths(self.pocketsphinx_dir, '', 'include')

        self.library_dirs += [ sysconfig.get_path('include') ]
        self.library_dirs += self.get_paths(self.ffmpeg_dir,       '', 'lib')
        self.library_dirs += self.get_paths(self.sphinxbase_dir,   '', 'lib')
        self.library_dirs += self.get_paths(self.pocketsphinx_dir, '', 'lib')

        if sys.platform == 'win32':
            bit64 = sys.maxsize > 2**32
            arch = 'x64' if bit64 else 'win32'
            self.include_dirs += self.get_paths(self.sphinxbase_dir,   os.path.join('include', 'win32'))
            self.library_dirs += self.get_paths(self.sphinxbase_dir,   os.path.join('bin', 'Release', arch))
            self.library_dirs += self.get_paths(self.pocketsphinx_dir, os.path.join('bin', 'Release', arch))

    def build_extensions(self):
        if self.compiler.compiler_type in ['unix', 'cygwin', 'mingw32']:
            self.setup_unix()

        elif self.compiler.compiler_type == 'msvc':
            self.setup_msvc()

        super().build_extensions()

    def setup_unix(self):
        self.cflags += [ '-O3', '-g' ]

        if sys.platform == 'darwin':
            self.cflags += ['-stdlib=libc++', '-mmacosx-version-min=10.7']

        if self.has_flag('-std=c++14'):
            self.cflags += ['-std=c++14']
        else:
            self.cflags += ['-std=c++11']

        optional_flags = [
                '-fvisibility=hidden',
                '-fomit-frame-pointer',
                '-fexpensive-optimizations',
                ]

        self.cflags += [ fl for fl in optional_flags if self.has_flag(fl) ]

        for ext in self.extensions:
            ext.extra_compile_args = self.cflags
            ext.extra_link_args    = self.ldflags

    def setup_msvc(self):
        self.cflags += [ '/EHsc' ]

    def get_paths(self, base_dir, *suffixes):
        if base_dir:
            res = [ os.path.realpath(os.path.join(base_dir, p)) for p in suffixes ]
            return [ p for p in res if os.path.isdir(p) ]
        else:
            return []

    def has_pkg_config(self):
        try:
            cmd = ['pkg-config', '--version']
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            proc.communicate()
            proc.wait()
            return True
        except FileNotFoundError:
            return False

    def get_pkg_config(self, method, modules, strip_prefixes=[]):
        cmd = ['pkg-config', method] + modules
        try:
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            out, err = proc.communicate()
            status = proc.wait()
            if status:
                print(' '.join(cmd))
                print('FAILED with status {}'.format(status))
                print(err.decode('utf8'))
                exit(1)

            res = []
            for param in out.decode('utf8').split():
                for prefix in strip_prefixes:
                    if param.startswith(prefix):
                        param = param[ len(prefix): ]
                res.append(param)
            return res

        except Exception as e:
            print(' '.join(cmd))
            print('FAILED: {}'.format(str(e)))
            exit(1)

    def has_flag(self, flagname):
        with tempfile.NamedTemporaryFile('w', suffix='.cpp') as f:
            f.write('int main (int argc, char **argv) { return 0; }')
            try:
                self.compiler.compile([f.name], extra_postargs=[flagname])
            except setuptools.distutils.errors.CompileError:
                return False
        return True


class gen_gui(distutils.cmd.Command):
    description = 'Generate GUI files'
    user_options = [
            ('wxformbuilder=', None, 'path to wxFormBuilder binary'),
            ]

    def initialize_options(self):
        self.wxformbuilder = 'wxformbuilder'

    def finalize_options(self):
        pass

    def run(self):
        files = glob.glob(os.path.join('subsync', 'gui', 'layout', '*.fbp'))
        excludeRe = self._mk_pattern('import gettext', '_ = gettext.gettext')

        try:
            for src in files:
                subprocess.check_call([self.wxformbuilder, '-g', src])

                dst = src[0:-4] + '.py'
                lines = []

                with open(dst, 'r') as fp:
                    for line in fp:
                        if not excludeRe.match(line):
                            lines.append(line)

                with open(dst, 'w') as fp:
                    for line in lines:
                        fp.write(line)

        except FileNotFoundError:
            print('Make sure you have installed wxFormBuilder')
            raise

    def _mk_pattern(self, *args):
        pattern = '|'.join([ '^{}$\\s'.format(x.replace(' ', '\\s')) for x in args ])
        return re.compile(pattern)


class gen_locales(distutils.cmd.Command):
    description = 'Generate locales file'
    user_options = [
            ('xgettext=', None, 'path to xgettext binary'),
            ('out=', None, 'path to output messages file'),
            ]

    def initialize_options(self):
        self.xgettext = 'xgettext'
        self.out_path = os.path.join('subsync', 'messages.po')

    def finalize_options(self):
        pass

    def run(self):
        try:
            files = glob.glob(os.path.join('subsync', '**', '*.py'), recursive=True)
            cmd = [self.xgettext, '--language=Python', '-o', self.out_path] + sorted(files)
            subprocess.check_call(cmd)

        except FileNotFoundError:
            print('Make sure you have installed xgettext')
            raise


class gen_version(distutils.cmd.Command):
    description = 'Generate version file'
    user_options = [
            ('out=', None, 'path to version file')
            ]

    def initialize_options(self):
        self.out_path = None

    def finalize_options(self):
        pass

    def run(self):
        version_long, version = read_version()
        write_version_file(version_long, version, self.out_path)


class gen_doc(distutils.cmd.Command):
    description = 'Generate API documentation'
    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        try:
            subprocess.check_call([
                'pdoc',
                '--html', 'subsync',
                '--output-dir', os.path.join('doc', 'api'),
                '--force',
                '--config=docformat="numpy"',
                ])
        except FileNotFoundError:
            print('Make sure you have installed pdoc3 package')
            raise


def read_version():
    try:
        version_long = subprocess.check_output(['git', 'describe', '--tags']).decode('UTF-8').strip()
        v = version_long[re.search('\d', version_long).start():].split('-')
        if len(v) > 1:
            version = '{}.{}'.format(v[0], v[1])
        else:
            version = '{}.0'.format(v[0])
    except Exception as e:
        print('Version not recognized, using default, reason: ' + str(e))
        version_long = 'custom'
        version = '0.0.0'

    return version_long, version


def write_version_file(version_long, version, path=None):
    if path is None:
        path = os.path.join('subsync', 'version.py')

    print('writing version number {} -> {}'.format(version_long, path))
    with open(path, 'w') as fp:
        fp.write('version = "{}"\n'.format(version_long))
        fp.write('version_short = "{}"\n'.format(version))


setup(
        name = 'subsync',
        version = read_version()[1],
        author = 'Michał Szymaniak',
        author_email = 'sc0typl@gmail.com',
        url = 'https://github.com/sc0ty/subsync',
        description = 'Subtitle Speech Synchronizer',
        license='GPLv3',
        python_requires = '>=3.5',
        packages = [
            'subsync',
            'subsync.synchro',
            'subsync.assets',
            'subsync.data',
            'subsync.gui',
            'subsync.gui.layout',
            'subsync.gui.components',
            'subsync.gui.components.batchlist',
            'gizmo'
            ],
        entry_points = {'gui_scripts': ['subsync=subsync.__main__:subsync']},
        setup_requires = [
            'pybind11>=2.4',
            ],
        install_requires = [
            'certifi',
            'pysubs2>=0.2.4',
            'pycryptodome>=3.9;platform_system!="Darwin"',
            'cryptography>=2.8;platform_system=="Darwin"',
            'PyYAML',
            'pybind11>=2.4',
            'requests>=2.0',
            ],
        extras_require = {
            'GUI': [ 'wxPython>=4.0' ],
            },
        scripts = ['bin/subsync'],
        cmdclass = {
            'build_py': build_py,
            'build_ext': build_ext,
            'gen_gui': gen_gui,
            'gen_locales': gen_locales,
            'gen_version': gen_version,
            'gen_doc': gen_doc,
            },
        package_data = {
            'subsync': [
                'key.pub',
                os.path.join('img', '*.png'),
                os.path.join('img', '*.ico'),
                os.path.join('locale', '*', 'LC_MESSAGES', '*.mo'),
                ],
            },
        ext_modules = [
            Extension('gizmo',
                sources = [
                    'gizmo/extractor.cpp',
                    'gizmo/correlator.cpp',
                    'gizmo/synchro/synchronizer.cpp',
                    'gizmo/media/demux.cpp',
                    'gizmo/media/stream.cpp',
                    'gizmo/media/audiodec.cpp',
                    'gizmo/media/resampler.cpp',
                    'gizmo/media/subdec.cpp',
                    'gizmo/media/speechrec.cpp',
                    'gizmo/text/translator.cpp',
                    'gizmo/text/dictionary.cpp',
                    'gizmo/text/words.cpp',
                    'gizmo/text/ssa.cpp',
                    'gizmo/text/ngrams.cpp',
                    'gizmo/text/utf8.cpp',
                    'gizmo/text/wordsqueue.cpp',
                    'gizmo/math/point.cpp',
                    'gizmo/math/line.cpp',
                    'gizmo/math/linefinder.cpp',
                    'gizmo/general/exception.cpp',
                    'gizmo/general/logger.cpp',
                    'gizmo/general/platform.cpp',
                    'gizmo/python/wrapper.cpp',
                    'gizmo/python/general.cpp',
                    'gizmo/python/extractor.cpp',
                    'gizmo/python/media.cpp',
                    'gizmo/python/stream.cpp',
                    'gizmo/python/correlator.cpp',
                    'gizmo/python/translator.cpp',
                    ],
                include_dirs = [ 'gizmo' ],
                define_macros = [
                    ('NDEBUG', '1'),
                    ('USE_PYBIND11', '1'),
                    ],
                language = 'c++',
                ),
            ]
        )