1from distutils.ccompiler import new_compiler as _new_compiler
2from distutils.command.clean import clean, log
3from distutils.core import Command
4from distutils.dir_util import remove_tree
5from distutils.errors import DistutilsExecError
6from distutils.msvccompiler import MSVCCompiler
7from setuptools import setup, find_packages, Extension, Distribution
8from setuptools.command.build_ext import build_ext
9from shlex import quote
10from subprocess import Popen, PIPE
11import argparse
12import errno
13import os
14import platform
15import re
16import shlex
17import sys
18
19try:
20    # This depends on _winreg, which is not availible on not-Windows.
21    from distutils.msvc9compiler import MSVCCompiler as MSVC9Compiler
22except ImportError:
23    MSVC9Compiler = None
24try:
25    from distutils._msvccompiler import MSVCCompiler as MSVC14Compiler
26except ImportError:
27    MSVC14Compiler = None
28
29try:
30    from Cython import __version__ as cython_version
31    from Cython.Build import cythonize
32except ImportError:
33    cythonize = None
34else:
35    # We depend upon some features in Cython 0.27; reject older ones.
36    if tuple(map(int, cython_version.split('.'))) < (0, 27):
37        print("Cython {} is too old for PyAV; ignoring it.".format(cython_version))
38        cythonize = None
39
40
41# We will embed this metadata into the package so it can be recalled for debugging.
42version = open('VERSION.txt').read().strip()
43try:
44    git_commit, _ = Popen(['git', 'describe', '--tags'], stdout=PIPE, stderr=PIPE).communicate()
45except OSError:
46    git_commit = None
47else:
48    git_commit = git_commit.decode().strip()
49
50
51_cflag_parser = argparse.ArgumentParser(add_help=False)
52_cflag_parser.add_argument('-I', dest='include_dirs', action='append')
53_cflag_parser.add_argument('-L', dest='library_dirs', action='append')
54_cflag_parser.add_argument('-l', dest='libraries', action='append')
55_cflag_parser.add_argument('-D', dest='define_macros', action='append')
56_cflag_parser.add_argument('-R', dest='runtime_library_dirs', action='append')
57def parse_cflags(raw_cflags):
58    raw_args = shlex.split(raw_cflags.strip())
59    args, unknown = _cflag_parser.parse_known_args(raw_args)
60    config = {k: v or [] for k, v in args.__dict__.items()}
61    for i, x in enumerate(config['define_macros']):
62        parts = x.split('=', 1)
63        value = x[1] or None if len(x) == 2 else None
64        config['define_macros'][i] = (parts[0], value)
65    return config, ' '.join(quote(x) for x in unknown)
66
67def get_library_config(name):
68    """Get distutils-compatible extension extras for the given library.
69
70    This requires ``pkg-config``.
71
72    """
73    try:
74        proc = Popen(['pkg-config', '--cflags', '--libs', name], stdout=PIPE, stderr=PIPE)
75    except OSError:
76        print('pkg-config is required for building PyAV')
77        exit(1)
78
79    raw_cflags, err = proc.communicate()
80    if proc.wait():
81        return
82
83    known, unknown = parse_cflags(raw_cflags.decode('utf8'))
84    if unknown:
85        print("pkg-config returned flags we don't understand: {}".format(unknown))
86        exit(1)
87
88    return known
89
90
91def update_extend(dst, src):
92    """Update the `dst` with the `src`, extending values where lists.
93
94    Primiarily useful for integrating results from `get_library_config`.
95
96    """
97    for k, v in src.items():
98        existing = dst.setdefault(k, [])
99        for x in v:
100            if x not in existing:
101                existing.append(x)
102
103
104def unique_extend(a, *args):
105    a[:] = list(set().union(a, *args))
106
107
108# Obtain the ffmpeg dir from the "--ffmpeg-dir=<dir>" argument
109FFMPEG_DIR = None
110for i, arg in enumerate(sys.argv):
111    if arg.startswith('--ffmpeg-dir='):
112        FFMPEG_DIR = arg.split('=')[1]
113        break
114
115if FFMPEG_DIR is not None:
116    # delete the --ffmpeg-dir arg so that distutils does not see it
117    del sys.argv[i]
118    if not os.path.isdir(FFMPEG_DIR):
119        print('The specified ffmpeg directory does not exist')
120        exit(1)
121else:
122    # Check the environment variable FFMPEG_DIR
123    FFMPEG_DIR = os.environ.get('FFMPEG_DIR')
124    if FFMPEG_DIR is not None:
125        if not os.path.isdir(FFMPEG_DIR):
126            FFMPEG_DIR = None
127
128if FFMPEG_DIR is not None:
129    ffmpeg_lib = os.path.join(FFMPEG_DIR, 'lib')
130    ffmpeg_include = os.path.join(FFMPEG_DIR, 'include')
131    if os.path.exists(ffmpeg_lib):
132        ffmpeg_lib = [ffmpeg_lib]
133    else:
134        ffmpeg_lib = [FFMPEG_DIR]
135    if os.path.exists(ffmpeg_include):
136        ffmpeg_include = [ffmpeg_include]
137    else:
138        ffmpeg_include = [FFMPEG_DIR]
139else:
140    ffmpeg_lib = []
141    ffmpeg_include = []
142
143
144# The "extras" to be supplied to every one of our modules.
145# This is expanded heavily by the `config` command.
146extension_extra = {
147    'include_dirs': ['include'] + ffmpeg_include,  # The first are PyAV's includes.
148    'libraries'   : [],
149    'library_dirs': ffmpeg_lib,
150}
151
152# The macros which describe the current PyAV version.
153config_macros = {
154    "PYAV_VERSION": version,
155    "PYAV_VERSION_STR": '"%s"' % version,
156    "PYAV_COMMIT_STR": '"%s"' % (git_commit or 'unknown-commit'),
157}
158
159
160def dump_config():
161    """Print out all the config information we have so far (for debugging)."""
162    print('PyAV:', version, git_commit or '(unknown commit)')
163    print('Python:', sys.version.encode('unicode_escape').decode())
164    print('platform:', platform.platform())
165    print('extension_extra:')
166    for k, vs in extension_extra.items():
167        print('\t%s: %s' % (k, [x.encode('utf8') for x in vs]))
168    print('config_macros:')
169    for x in sorted(config_macros.items()):
170        print('\t%s=%s' % x)
171
172
173# Monkey-patch for CCompiler to be silent.
174def _CCompiler_spawn_silent(cmd, dry_run=None):
175    """Spawn a process, and eat the stdio."""
176    proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
177    out, err = proc.communicate()
178    if proc.returncode:
179        raise DistutilsExecError(err)
180
181def new_compiler(*args, **kwargs):
182    """Create a C compiler.
183
184    :param bool silent: Eat all stdio? Defaults to ``True``.
185
186    All other arguments passed to ``distutils.ccompiler.new_compiler``.
187
188    """
189    make_silent = kwargs.pop('silent', True)
190    cc = _new_compiler(*args, **kwargs)
191    # If MSVC10, initialize the compiler here and add /MANIFEST to linker flags.
192    # See Python issue 4431 (https://bugs.python.org/issue4431)
193    if is_msvc(cc):
194        from distutils.msvc9compiler import get_build_version
195        if get_build_version() == 10:
196            cc.initialize()
197            for ldflags in [cc.ldflags_shared, cc.ldflags_shared_debug]:
198                unique_extend(ldflags, ['/MANIFEST'])
199        # If MSVC14, do not silence. As msvc14 requires some custom
200        # steps before the process is spawned, we can't monkey-patch this.
201        elif get_build_version() == 14:
202            make_silent = False
203    # monkey-patch compiler to suppress stdout and stderr.
204    if make_silent:
205        cc.spawn = _CCompiler_spawn_silent
206    return cc
207
208
209_msvc_classes = tuple(filter(None, (MSVCCompiler, MSVC9Compiler, MSVC14Compiler)))
210def is_msvc(cc=None):
211    cc = _new_compiler() if cc is None else cc
212    return isinstance(cc, _msvc_classes)
213
214
215if os.name == 'nt':
216
217    if is_msvc():
218        config_macros['inline'] = '__inline'
219
220    # Since we're shipping a self contained unit on Windows, we need to mark
221    # the package as such. On other systems, let it be universal.
222    class BinaryDistribution(Distribution):
223        def is_pure(self):
224            return False
225
226    distclass = BinaryDistribution
227
228else:
229
230    # Nothing to see here.
231    distclass = Distribution
232
233
234# Monkey-patch Cython to not overwrite embedded signatures.
235if cythonize:
236
237    from Cython.Compiler.AutoDocTransforms import EmbedSignature
238
239    old_embed_signature = EmbedSignature._embed_signature
240    def new_embed_signature(self, sig, doc):
241
242        # Strip any `self` parameters from the front.
243        sig = re.sub(r'\(self(,\s+)?', '(', sig)
244
245        # If they both start with the same signature; skip it.
246        if sig and doc:
247            new_name = sig.split('(')[0].strip()
248            old_name = doc.split('(')[0].strip()
249            if new_name == old_name:
250                return doc
251            if new_name.endswith('.' + old_name):
252                return doc
253
254        return old_embed_signature(self, sig, doc)
255
256    EmbedSignature._embed_signature = new_embed_signature
257
258
259# Construct the modules that we find in the "av" directory.
260ext_modules = []
261for dirname, dirnames, filenames in os.walk('av'):
262    for filename in filenames:
263
264        # We are looing for Cython sources.
265        if filename.startswith('.') or os.path.splitext(filename)[1] != '.pyx':
266            continue
267
268        pyx_path = os.path.join(dirname, filename)
269        base = os.path.splitext(pyx_path)[0]
270
271        # Need to be a little careful because Windows will accept / or \
272        # (where os.sep will be \ on Windows).
273        mod_name = base.replace('/', '.').replace(os.sep, '.')
274
275        c_path = os.path.join('src', base + '.c')
276
277        # We go with the C sources if Cython is not installed, and fail if
278        # those also don't exist. We can't `cythonize` here though, since the
279        # `pyav/include.h` must be generated (by `build_ext`) first.
280        if not cythonize and not os.path.exists(c_path):
281            print('Cython is required to build PyAV from raw sources.')
282            print('Please `pip install Cython`.')
283            exit(3)
284        ext_modules.append(Extension(
285            mod_name,
286            sources=[c_path if not cythonize else pyx_path],
287        ))
288
289
290class ConfigCommand(Command):
291
292    user_options = [
293        ('no-pkg-config', None,
294         "do not use pkg-config to configure dependencies"),
295        ('verbose', None,
296         "dump out configuration"),
297        ('compiler=', 'c',
298         "specify the compiler type"), ]
299
300    boolean_options = ['no-pkg-config']
301
302    def initialize_options(self):
303        self.compiler = None
304        self.no_pkg_config = None
305
306    def finalize_options(self):
307        self.set_undefined_options('build',
308            ('compiler', 'compiler'),)
309        self.set_undefined_options('build_ext',
310            ('no_pkg_config', 'no_pkg_config'),)
311
312    def run(self):
313
314        # For some reason we get the feeling that CFLAGS is not respected, so we parse
315        # it here. TODO: Leave any arguments that we can't figure out.
316        for name in 'CFLAGS', 'LDFLAGS':
317            known, unknown = parse_cflags(os.environ.pop(name, ''))
318            if unknown:
319                print("Warning: We don't understand some of {} (and will leave it in the envvar): {}".format(name, unknown))
320                os.environ[name] = unknown
321            update_extend(extension_extra, known)
322
323        if is_msvc(new_compiler(compiler=self.compiler)):
324            # Assume we have to disable /OPT:REF for MSVC with ffmpeg
325            config = {
326                'extra_link_args': ['/OPT:NOREF'],
327            }
328            update_extend(extension_extra, config)
329
330        # Check if we're using pkg-config or not
331        if self.no_pkg_config:
332            # Simply assume we have everything we need!
333            config = {
334                'libraries':    ['avformat', 'avcodec', 'avdevice', 'avutil', 'avfilter',
335                                 'swscale', 'swresample'],
336                'library_dirs': [],
337                'include_dirs': []
338            }
339            update_extend(extension_extra, config)
340            for ext in self.distribution.ext_modules:
341                for key, value in extension_extra.items():
342                    setattr(ext, key, value)
343            return
344
345        # We're using pkg-config:
346        errors = []
347
348        # Get the config for the libraries that we require.
349        for name in 'libavformat', 'libavcodec', 'libavdevice', 'libavutil', 'libavfilter', 'libswscale', 'libswresample':
350            config = get_library_config(name)
351            if config:
352                update_extend(extension_extra, config)
353                # We don't need macros for these, since they all must exist.
354            else:
355                errors.append('Could not find ' + name + ' with pkg-config.')
356
357        if self.verbose:
358            dump_config()
359
360        # Don't continue if we have errors.
361        # TODO: Warn Ubuntu 12 users that they can't satisfy requirements with the
362        # default package sources.
363        if errors:
364            print('\n'.join(errors))
365            exit(1)
366
367        # Normalize the extras.
368        extension_extra.update(
369            dict((k, sorted(set(v))) for k, v in extension_extra.items())
370        )
371
372        # Apply them.
373        for ext in self.distribution.ext_modules:
374            for key, value in extension_extra.items():
375                setattr(ext, key, value)
376
377
378class CleanCommand(clean):
379
380    user_options = clean.user_options + [
381        ('sources', None,
382         "remove Cython build output (C sources)")]
383
384    boolean_options = clean.boolean_options + ['sources']
385
386    def initialize_options(self):
387        clean.initialize_options(self)
388        self.sources = None
389
390    def run(self):
391        clean.run(self)
392        if self.sources:
393            if os.path.exists('src'):
394                remove_tree('src', dry_run=self.dry_run)
395            else:
396                log.info("'%s' does not exist -- can't clean it", 'src')
397
398
399class CythonizeCommand(Command):
400
401    user_options = []
402    def initialize_options(self):
403        pass
404    def finalize_options(self):
405        pass
406
407    def run(self):
408
409        # Cythonize, if required. We do it individually since we must update
410        # the existing extension instead of replacing them all.
411        for i, ext in enumerate(self.distribution.ext_modules):
412            if any(s.endswith('.pyx') for s in ext.sources):
413                if is_msvc():
414                    ext.define_macros.append(('inline', '__inline'))
415                new_ext = cythonize(
416                    ext,
417                    compiler_directives=dict(
418                        c_string_type='str',
419                        c_string_encoding='ascii',
420                        embedsignature=True,
421                        language_level=2,
422                    ),
423                    build_dir='src',
424                    include_path=ext.include_dirs,
425                )[0]
426                ext.sources = new_ext.sources
427
428
429class BuildExtCommand(build_ext):
430
431    if os.name != 'nt':
432        user_options = build_ext.user_options + [
433            ('no-pkg-config', None,
434             "do not use pkg-config to configure dependencies")]
435
436        boolean_options = build_ext.boolean_options + ['no-pkg-config']
437
438        def initialize_options(self):
439            build_ext.initialize_options(self)
440            self.no_pkg_config = None
441    else:
442        no_pkg_config = 1
443
444    def run(self):
445
446        # Propagate build options to config
447        obj = self.distribution.get_command_obj('config')
448        obj.compiler = self.compiler
449        obj.no_pkg_config = self.no_pkg_config
450        obj.include_dirs = self.include_dirs
451        obj.libraries = self.libraries
452        obj.library_dirs = self.library_dirs
453
454        self.run_command('config')
455
456        # We write a header file containing everything we have discovered by
457        # inspecting the libraries which exist. This is the main mechanism we
458        # use to detect differenced between FFmpeg and Libav.
459
460        include_dir = os.path.join(self.build_temp, 'include')
461        pyav_dir = os.path.join(include_dir, 'pyav')
462        try:
463            os.makedirs(pyav_dir)
464        except OSError as e:
465            if e.errno != errno.EEXIST:
466                raise
467        header_path = os.path.join(pyav_dir, 'config.h')
468        print('writing', header_path)
469        with open(header_path, 'w') as fh:
470            fh.write('#ifndef PYAV_COMPAT_H\n')
471            fh.write('#define PYAV_COMPAT_H\n')
472            for k, v in sorted(config_macros.items()):
473                fh.write('#define %s %s\n' % (k, v))
474            fh.write('#endif\n')
475
476        self.include_dirs = self.include_dirs or []
477        self.include_dirs.append(include_dir)
478        # Propagate config to cythonize.
479        for i, ext in enumerate(self.distribution.ext_modules):
480            unique_extend(ext.include_dirs, self.include_dirs)
481            unique_extend(ext.library_dirs, self.library_dirs)
482            unique_extend(ext.libraries, self.libraries)
483
484        self.run_command('cythonize')
485        build_ext.run(self)
486
487
488setup(
489
490    name='av',
491    version=version,
492    description="Pythonic bindings for FFmpeg's libraries.",
493
494    author="Mike Boers",
495    author_email="pyav@mikeboers.com",
496
497    url="https://github.com/PyAV-Org/PyAV",
498
499    packages=find_packages(exclude=['build*', 'examples*', 'scratchpad*', 'tests*']),
500
501    zip_safe=False,
502    ext_modules=ext_modules,
503
504    cmdclass={
505        'build_ext': BuildExtCommand,
506        'clean': CleanCommand,
507        'config': ConfigCommand,
508        'cythonize': CythonizeCommand,
509    },
510
511    test_suite='tests',
512
513    entry_points={
514        'console_scripts': [
515            'pyav = av.__main__:main',
516        ],
517    },
518
519    classifiers=[
520       'Development Status :: 5 - Production/Stable',
521       'Intended Audience :: Developers',
522       'License :: OSI Approved :: BSD License',
523       'Natural Language :: English',
524       'Operating System :: MacOS :: MacOS X',
525       'Operating System :: POSIX',
526       'Operating System :: Unix',
527       'Operating System :: Microsoft :: Windows',
528       'Programming Language :: Cython',
529       'Programming Language :: Python :: 3.5',
530       'Programming Language :: Python :: 3.6',
531       'Programming Language :: Python :: 3.7',
532       'Programming Language :: Python :: 3.8',
533       'Programming Language :: Python :: 3.9',
534       'Topic :: Software Development :: Libraries :: Python Modules',
535       'Topic :: Multimedia :: Sound/Audio',
536       'Topic :: Multimedia :: Sound/Audio :: Conversion',
537       'Topic :: Multimedia :: Video',
538       'Topic :: Multimedia :: Video :: Conversion',
539   ],
540
541    distclass=distclass,
542
543)
544