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