from __future__ import print_function from distutils.ccompiler import new_compiler as _new_compiler, LinkError, CompileError from distutils.command.clean import clean, log from distutils.core import Command from distutils.errors import DistutilsExecError from distutils.msvccompiler import MSVCCompiler from setuptools import setup, find_packages, Extension, Distribution from setuptools.command.build_ext import build_ext from distutils.command.clean import clean, log from distutils.dir_util import remove_tree from subprocess import Popen, PIPE import errno import itertools import json import os import platform import re import sys try: # This depends on _winreg, which is not availible on not-Windows. from distutils.msvc9compiler import MSVCCompiler as MSVC9Compiler except ImportError: MSVC9Compiler = None try: from distutils._msvccompiler import MSVCCompiler as MSVC14Compiler except ImportError: MSVC14Compiler = None msvc_compiler_classes = tuple([cls for cls in (MSVCCompiler, MSVC9Compiler, MSVC14Compiler) if cls is not None]) try: from Cython.Build import cythonize except ImportError: # We don't need Cython all the time; just for building from original source. cythonize = None is_py3 = sys.version_info[0] >= 3 # We will embed this metadata into the package so it can be recalled for debugging. version = '0.3.3' git_commit, _ = Popen(['git', 'describe', '--tags'], stdout=PIPE, stderr=PIPE).communicate() git_commit = git_commit.strip() def get_library_config(name): """Get distutils-compatible extension extras for the given library. This requires ``pkg-config``. """ try: proc = Popen(['pkg-config', '--cflags', '--libs', name], stdout=PIPE, stderr=PIPE) except OSError: print('pkg-config is required for building PyAV') exit(1) raw_config, err = proc.communicate() if proc.wait(): return config = {} for chunk in raw_config.decode('utf8').strip().split(): if chunk.startswith('-I'): config.setdefault('include_dirs', []).append(chunk[2:]) elif chunk.startswith('-L'): config.setdefault('library_dirs', []).append(chunk[2:]) elif chunk.startswith('-l'): config.setdefault('libraries', []).append(chunk[2:]) elif chunk.startswith('-D'): name = chunk[2:].split('=')[0] config.setdefault('define_macros', []).append((name, None)) return config def update_extend(dst, src): """Update the `dst` with the `src`, extending values where lists. Primiarily useful for integrating results from `get_library_config`. """ for k, v in src.items(): existing = dst.setdefault(k, []) for x in v: if x not in existing: existing.append(x) def unique_extend(a, *args): a[:] = list(set().union(a, *args)) def is_msvc(cc=None): cc = _new_compiler() if cc is None else cc return isinstance(cc, msvc_compiler_classes) # Obtain the ffmpeg dir from the "--ffmpeg-dir=" argument FFMPEG_DIR = None for i, arg in enumerate(sys.argv): if arg.startswith('--ffmpeg-dir='): FFMPEG_DIR = arg.split('=')[1] break if FFMPEG_DIR is not None: # delete the --ffmpeg-dir arg so that distutils does not see it del sys.argv[i] if not os.path.isdir(FFMPEG_DIR): print('The specified ffmpeg directory does not exist') exit(1) else: # Check the environment variable FFMPEG_DIR FFMPEG_DIR = os.environ.get('FFMPEG_DIR') if FFMPEG_DIR is not None: if not os.path.isdir(FFMPEG_DIR): FFMPEG_DIR = None if FFMPEG_DIR is not None: ffmpeg_lib = os.path.join(FFMPEG_DIR, 'lib') ffmpeg_include = os.path.join(FFMPEG_DIR, 'include') if os.path.exists(ffmpeg_lib): ffmpeg_lib = [ffmpeg_lib] else: ffmpeg_lib = [FFMPEG_DIR] if os.path.exists(ffmpeg_include): ffmpeg_include = [ffmpeg_include] else: ffmpeg_include = [FFMPEG_DIR] else: ffmpeg_lib = [] ffmpeg_include = [] # The "extras" to be supplied to every one of our modules. # This is expanded heavily by the `config` command. extension_extra = { 'include_dirs': ['include'] + ffmpeg_include, # The first are PyAV's includes. 'libraries' : [], 'library_dirs': ffmpeg_lib, } # The macros which describe what functions and structure members we have # from the underlying libraries. This is expanded heavily by `reflect` command. config_macros = [ ("PYAV_VERSION", version), ("PYAV_VERSION_STR", '"%s"' % version), ("PYAV_COMMIT_STR", '"%s"' % (git_commit or 'unknown-commit')) ] def dump_config(): """Print out all the config information we have so far (for debugging).""" print('PyAV:', version, git_commit) print('Python:', sys.version.encode('unicode_escape' if is_py3 else 'string-escape')) print('platform:', platform.platform()) print('extension_extra:') for k, vs in extension_extra.items(): print('\t%s: %s' % (k, [x.encode('utf8') for x in vs])) print('config_macros:') for x in config_macros: print('\t%s=%s' % x) if os.name == 'nt': if is_msvc(): config_macros.append(('inline', '__inline')) # Since we're shipping a self contained unit on windows, we need to mark # the package as such. On other systems, let it be universal. class BinaryDistribution(Distribution): def is_pure(self): return False distclass = BinaryDistribution else: distclass = Distribution # Monkey-patch for CCompiler to be silent. def _CCompiler_spawn_silent(cmd, dry_run=None): """Spawn a process, and eat the stdio.""" proc = Popen(cmd, stdout=PIPE, stderr=PIPE) out, err = proc.communicate() if proc.returncode: raise DistutilsExecError(err) def new_compiler(*args, **kwargs): """Create a C compiler. :param bool silent: Eat all stdio? Defaults to ``True``. All other arguments passed to ``distutils.ccompiler.new_compiler``. """ cc = _new_compiler(*args, **kwargs) make_silent = True # If MSVC10, initialize the compiler here and add /MANIFEST to linker flags. # See Python issue 4431 (https://bugs.python.org/issue4431) if is_msvc(cc): from distutils.msvc9compiler import get_build_version if get_build_version() == 10: cc.initialize() for ldflags in [cc.ldflags_shared, cc.ldflags_shared_debug]: unique_extend(ldflags, ['/MANIFEST']) # If MSVC14, do not silence. As msvc14 requires some custom # steps before the process is spawned, we can't monkey-patch this. elif get_build_version() == 14: make_silent = False # monkey-patch compiler to suppress stdout and stderr. if make_silent and kwargs.pop('silent', True): cc.spawn = _CCompiler_spawn_silent return cc def compile_check(code, name, includes=None, include_dirs=None, libraries=None, library_dirs=None, link=True, compiler=None): """Check that we can compile and link the given source. Caches results; delete the ``build`` directory to reset. Writes source (``*.c``), builds (``*.o``), executables (``*.out``), and cached results (``*.json``) in ``build/temp.$platform/reflection``. """ exec_path = name + '.out' source_path = name + '.c' result_path = name + '.json' if os.path.exists(result_path): try: return json.load(open(result_path)) except ValueError: pass cc = new_compiler(compiler=compiler) with open(source_path, 'w') as fh: if is_msvc(cc): fh.write("#define inline __inline\n") for include in includes or (): fh.write('#include "%s"\n' % include) fh.write('main(int argc, char **argv)\n{ %s; }\n' % code) try: objects = cc.compile([source_path], include_dirs=include_dirs) if link: cc.link_executable(objects, exec_path, libraries=libraries, library_dirs=library_dirs) except (CompileError, LinkError, TypeError): res = False else: res = True with open(result_path, 'w') as fh: fh.write(json.dumps(res)) return res # Monkey-patch Cython to not overwrite embedded signatures. if cythonize: from Cython.Compiler.AutoDocTransforms import EmbedSignature old_embed_signature = EmbedSignature._embed_signature def new_embed_signature(self, sig, doc): # Strip any `self` parameters from the front. sig = re.sub(r'\(self(,\s+)?', '(', sig) # If they both start with the same signature; skip it. if sig and doc: new_name = sig.split('(')[0].strip() old_name = doc.split('(')[0].strip() if new_name == old_name: return doc if new_name.endswith('.' + old_name): return doc return old_embed_signature(self, sig, doc) EmbedSignature._embed_signature = new_embed_signature # Construct the modules that we find in the "av" directory. ext_modules = [] for dirname, dirnames, filenames in os.walk('av'): for filename in filenames: # We are looing for Cython sources. if filename.startswith('.') or os.path.splitext(filename)[1] != '.pyx': continue pyx_path = os.path.join(dirname, filename) base = os.path.splitext(pyx_path)[0] # Need to be a little careful because Windows will accept / or \ # (where os.sep will be \ on Windows). mod_name = base.replace('/', '.').replace(os.sep, '.') c_path = os.path.join('src', base + '.c') # We go with the C sources if Cython is not installed, and fail if # those also don't exist. We can't `cythonize` here though, since the # `pyav/include.h` must be generated (by `build_ext`) first. if not cythonize and not os.path.exists(c_path): print('Cython is required to build PyAV from raw sources.') print('Please `pip install Cython`.') exit(3) ext_modules.append(Extension( mod_name, sources=[c_path if not cythonize else pyx_path], )) class ConfigCommand(Command): user_options = [ ('no-pkg-config', None, "do not use pkg-config to configure dependencies"), ('compiler=', 'c', "specify the compiler type"), ] boolean_options = ['no-pkg-config'] def initialize_options(self): self.compiler = None self.no_pkg_config = None def finalize_options(self): self.set_undefined_options('build', ('compiler', 'compiler'),) self.set_undefined_options('build_ext', ('no_pkg_config', 'no_pkg_config'),) def run(self): if is_msvc(new_compiler(compiler=self.compiler)): # Assume we have to disable /OPT:REF for MSVC with ffmpeg config = { 'extra_link_args': ['/OPT:NOREF'], } update_extend(extension_extra, config) # Check if we're using pkg-config or not if self.no_pkg_config: # Simply assume we have everything we need! config = { 'libraries': ['avformat', 'avcodec', 'avdevice', 'avutil', 'avfilter', 'swscale'], 'library_dirs': [], 'include_dirs': [] } config['libraries'].append('swresample') config_macros.append(('PYAV_HAVE_LIBSWRESAMPLE', 1)) update_extend(extension_extra, config) for ext in self.distribution.ext_modules: for key, value in extension_extra.items(): setattr(ext, key, value) return # We're using pkg-config: errors = [] # Get the config for the libraries that we require. for name in 'libavformat', 'libavcodec', 'libavdevice', 'libavutil', 'libavfilter', 'libswscale': config = get_library_config(name) if config: update_extend(extension_extra, config) # We don't need macros for these, since they all must exist. else: errors.append('Could not find ' + name + ' with pkg-config.') # Get the config for either swresample OR avresample. for name in 'libswresample', 'libavresample': config = get_library_config(name) if config: update_extend(extension_extra, config) config_macros.append(('PYAV_HAVE_' + name.upper(), '1')) break else: errors.append('Could not find either libswresample or libavresample with pkg-config.') # Don't continue if we have errors. # TODO: Warn Ubuntu 12 users that they can't satisfy requirements with the # default package sources. if errors: print('\n'.join(errors)) exit(1) # Normalize the extras. extension_extra.update( dict((k, sorted(set(v))) for k, v in extension_extra.items()) ) for ext in self.distribution.ext_modules: for key, value in extension_extra.items(): setattr(ext, key, value) class ReflectCommand(Command): sep_by = " (separated by '%s')" % os.pathsep user_options = [ ('build-temp=', 't', "directory for temporary files (build by-products)"), ('include-dirs=', 'I', "list of directories to search for header files" + sep_by), ('libraries=', 'l', "external C libraries to link with"), ('library-dirs=', 'L', "directories to search for external C libraries" + sep_by), ('no-pkg-config', None, "do not use pkg-config to configure dependencies"), ('compiler=', 'c', "specify the compiler type"), ] boolean_options = ['no-pkg-config'] def initialize_options(self): self.compiler = None self.build_temp = None self.include_dirs = None self.libraries = None self.library_dirs = None self.no_pkg_config = None def finalize_options(self): self.set_undefined_options('build', ('build_temp', 'build_temp'), ('compiler', 'compiler'), ) self.set_undefined_options('build_ext', ('include_dirs', 'include_dirs'), ('libraries', 'libraries'), ('library_dirs', 'library_dirs'), ('no_pkg_config', 'no_pkg_config'), ) # Need to do this ourself, since no inheritance from build_ext: try: str_base = basestring except NameError: str_base = str if isinstance(self.include_dirs, str_base): self.include_dirs = self.include_dirs.split(os.pathsep) if isinstance(self.library_dirs, str_base): self.library_dirs = str.split(self.library_dirs, os.pathsep) def run(self): # Propagate options obj = self.distribution.get_command_obj('config') obj.no_pkg_config = self.no_pkg_config obj.compiler = self.compiler self.run_command('config') tmp_dir = os.path.join(self.build_temp, 'reflection') try: os.makedirs(tmp_dir) except OSError as e: if e.errno != errno.EEXIST: raise found = [] reflection_includes = [ 'libavcodec/avcodec.h', 'libavformat/avformat.h', 'libavutil/avutil.h', ] config = extension_extra.copy() config['include_dirs'] += self.include_dirs config['libraries'] += self.libraries config['library_dirs'] += self.library_dirs # Check for some specific functions. for func_name in ( 'avformat_open_input', # Canary that should exist. 'pyav_function_should_not_exist', # Canary that should not exist. # This we actually care about: 'av_calloc', 'av_frame_get_best_effort_timestamp', 'avformat_alloc_output_context2', 'avformat_close_input', 'avcodec_send_packet', ): print("looking for %s... " % func_name, end='') if compile_check( name=os.path.join(tmp_dir, func_name), code='%s()' % func_name, libraries=config['libraries'], library_dirs=config['library_dirs'], compiler=self.compiler, ): print('found') found.append(func_name) else: print('missing') for struct_name, member_name in ( ('AVStream', 'index'), # Canary that should exist ('PyAV', 'struct_should_not_exist'), # Canary that should not exist. # Things we actually care about: ('AVFrame', 'mb_type'), ): print("looking for %s.%s... " % (struct_name, member_name), end='') if compile_check( name=os.path.join(tmp_dir, '%s.%s' % (struct_name, member_name)), code='struct %s x; x.%s;' % (struct_name, member_name), includes=reflection_includes, include_dirs=config['include_dirs'], link=False, compiler=self.compiler, ): print('found') # Double-unscores for members. found.append('%s__%s' % (struct_name, member_name)) else: print('missing') canaries = { 'pyav_function_should_not_exist': ('function', False), 'PyAV__struct_should_not_exist': ('member', False), 'avformat_open_input': ('function', True), 'AVStream__index': ('member', True), } # Create macros for the things that we found. # There is potential for naming collisions between functions and # structure members, but until we actually have one, we won't # worry about it. config_macros.extend( ('PYAV_HAVE_%s' % name.upper(), '1') for name in found if name not in canaries ) # Make sure our canaries report back properly. for name, (type_, should_exist) in canaries.items(): if should_exist != (name in found): print('\nWe %s `%s` in the libraries.' % ( 'didn\'t find' if should_exist else 'found', name )) print('We look for it only as a sanity check to make sure the build\n' 'process is working as expected. It is not, so we must abort.\n' '\n' 'Please open a ticket at https://github.com/mikeboers/PyAV/issues\n' 'with the folowing information:\n') dump_config() exit(1) class DoctorCommand(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): self.run_command('config') self.run_command('reflect') print() dump_config() class CleanCommand(clean): user_options = clean.user_options + [ ('sources', None, "remove Cython build output (C sources)")] boolean_options = clean.boolean_options + ['sources'] def initialize_options(self): clean.initialize_options(self) self.sources = None def run(self): clean.run(self) if self.sources: if os.path.exists('src'): remove_tree('src', dry_run=self.dry_run) else: log.info("'%s' does not exist -- can't clean it", 'src') class CythonizeCommand(Command): user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): # Cythonize, if required. We do it individually since we must update # the existing extension instead of replacing them all. for i, ext in enumerate(self.distribution.ext_modules): if any(s.endswith('.pyx') for s in ext.sources): if is_msvc(): ext.define_macros.append(('inline', '__inline')) new_ext = cythonize( ext, compiler_directives=dict( c_string_type='str', c_string_encoding='ascii', embedsignature=True, ), build_dir='src', include_path=ext.include_dirs, )[0] ext.sources = new_ext.sources class BuildExtCommand(build_ext): if os.name != 'nt': user_options = build_ext.user_options + [ ('no-pkg-config', None, "do not use pkg-config to configure dependencies")] boolean_options = build_ext.boolean_options + ['no-pkg-config'] def initialize_options(self): build_ext.initialize_options(self) self.no_pkg_config = None else: no_pkg_config = 1 def run(self): # Propagate build options to reflect obj = self.distribution.get_command_obj('reflect') obj.compiler = self.compiler obj.no_pkg_config = self.no_pkg_config obj.include_dirs = self.include_dirs obj.libraries = self.libraries obj.library_dirs = self.library_dirs self.run_command('reflect') # We write a header file containing everything we have discovered by # inspecting the libraries which exist. This is the main mechanism we # use to detect differenced between FFmpeg and Libav. include_dir = os.path.join(self.build_temp, 'include') pyav_dir = os.path.join(include_dir, 'pyav') try: os.makedirs(pyav_dir) except OSError as e: if e.errno != errno.EEXIST: raise header_path = os.path.join(pyav_dir, 'config.h') print('writing', header_path) with open(header_path, 'w') as fh: fh.write('#ifndef PYAV_COMPAT_H\n') fh.write('#define PYAV_COMPAT_H\n') for k, v in config_macros: fh.write('#define %s %s\n' % (k, v)) fh.write('#endif\n') self.include_dirs = self.include_dirs or [] self.include_dirs.append(include_dir) # Propagate config to cythonize. for i, ext in enumerate(self.distribution.ext_modules): unique_extend(ext.include_dirs, self.include_dirs) unique_extend(ext.library_dirs, self.library_dirs) unique_extend(ext.libraries, self.libraries) self.run_command('cythonize') return build_ext.run(self) setup( name='av', version=version, description='Pythonic bindings for FFmpeg/Libav.', author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/mikeboers/PyAV", packages=find_packages(exclude=['build*', 'tests*', 'examples*']), zip_safe=False, ext_modules=ext_modules, cmdclass={ 'build_ext': BuildExtCommand, 'clean': CleanCommand, 'config': ConfigCommand, 'cythonize': CythonizeCommand, 'doctor': DoctorCommand, 'reflect': ReflectCommand, }, test_suite='nose.collector', entry_points={ 'console_scripts': [ 'pyav = av.__main__:main', ], }, classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', 'Operating System :: Unix', 'Programming Language :: Cython', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio :: Conversion', 'Topic :: Multimedia :: Video', 'Topic :: Multimedia :: Video :: Conversion', ], distclass=distclass, )