1# python-gphoto2 - Python interface to libgphoto2 2# http://github.com/jim-easterbrook/python-gphoto2 3# Copyright (C) 2014-20 Jim Easterbrook jim@jim-easterbrook.me.uk 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18from collections import defaultdict 19from distutils.cmd import Command 20from distutils.command.upload import upload as _upload 21from distutils.core import setup, Extension 22from distutils.log import error 23import os 24import re 25import subprocess 26import sys 27 28# python-gphoto2 version 29version = '2.2.4' 30 31# get gphoto2 library config 32cmd = ['pkg-config', '--modversion', 'libgphoto2'] 33FNULL = open(os.devnull, 'w') 34try: 35 gphoto2_version = subprocess.check_output( 36 cmd, stderr=FNULL, universal_newlines=True).split('.')[:3] 37 gphoto2_version = tuple(map(int, gphoto2_version)) 38except Exception: 39 error('ERROR: command "%s" failed', ' '.join(cmd)) 40 raise 41gphoto2_flags = defaultdict(list) 42for flag in subprocess.check_output( 43 ['pkg-config', '--cflags', '--libs', 'libgphoto2'], 44 universal_newlines=True).split(): 45 gphoto2_flags[flag[:2]].append(flag) 46gphoto2_include = gphoto2_flags['-I'] 47gphoto2_libs = gphoto2_flags['-l'] 48gphoto2_lib_dirs = gphoto2_flags['-L'] 49for n in range(len(gphoto2_include)): 50 if gphoto2_include[n].endswith('/gphoto2'): 51 gphoto2_include[n] = gphoto2_include[n][:-len('/gphoto2')] 52 53# create extension modules list 54ext_modules = [] 55info_file = os.path.join('src', 'info.txt') 56if os.path.exists(info_file): 57 with open(info_file) as src: 58 code = compile(src.read(), info_file, 'exec') 59 exec(code, globals(), locals()) 60else: 61 swig_version = (0, 0, 0) 62use_builtin = (swig_version != (2, 0, 11) and 63 (swig_version >= (3, 0, 8) or sys.version_info < (3, 5))) 64if 'PYTHON_GPHOTO2_BUILTIN' in os.environ: 65 use_builtin = True 66if 'PYTHON_GPHOTO2_NO_BUILTIN' in os.environ: 67 use_builtin = False 68mod_src_dir = 'swig' 69if use_builtin: 70 mod_src_dir += '-bi' 71mod_src_dir += '-py' + str(sys.version_info[0]) 72mod_src_dir += '-gp' + '.'.join(map(str, gphoto2_version[:2])) 73mod_src_dir = os.path.join('src', mod_src_dir) 74 75extra_compile_args = [ 76 '-Wno-unused-label', '-Wno-strict-prototypes', 77 '-DGPHOTO2_VERSION=' + '0x{:02x}{:02x}{:02x}'.format(*gphoto2_version)] 78if 'PYTHON_GPHOTO2_STRICT' in os.environ: 79 extra_compile_args.append('-Werror') 80libraries = [x.replace('-l', '') for x in gphoto2_libs] 81library_dirs = [x.replace('-L', '') for x in gphoto2_lib_dirs] 82include_dirs = [x.replace('-I', '') for x in gphoto2_include] 83if os.path.isdir(mod_src_dir): 84 for file_name in os.listdir(mod_src_dir): 85 if file_name[-7:] != '_wrap.c': 86 continue 87 ext_name = file_name[:-7] 88 ext_modules.append(Extension( 89 '_' + ext_name, 90 sources = [os.path.join(mod_src_dir, file_name)], 91 libraries = libraries, 92 library_dirs = library_dirs, 93 runtime_library_dirs = library_dirs, 94 include_dirs = include_dirs, 95 extra_compile_args = extra_compile_args, 96 )) 97 98cmdclass = {} 99command_options = {} 100 101def get_gp_versions(): 102 # get gphoto2 versions to be swigged 103 gp_versions = [] 104 for name in os.listdir('.'): 105 match = re.match('libgphoto2-(.*)', name) 106 if match: 107 gp_versions.append(match.group(1)) 108 gp_versions.sort() 109 if not gp_versions: 110 gp_versions = ['.'.join(map(str, gphoto2_version[:2]))] 111 return gp_versions 112 113# add command to run doxygen and doxy2swig 114member_methods = ( 115 ('gp_abilities_list_', '_CameraAbilitiesList', 'CameraAbilitiesList'), 116 ('gp_camera_', '_Camera', 'Camera'), 117 ('gp_context_', '_GPContext', 'Context'), 118 ('gp_file_', '_CameraFile', 'CameraFile'), 119 ('gp_list_', '_CameraList', 'CameraList'), 120 ('gp_port_info_list_', '_GPPortInfoList', 'PortInfoList'), 121 ('gp_port_info_', '_GPPortInfo', 'PortInfo'), 122 ('gp_widget_', '_CameraWidget', 'CameraWidget'), 123 ) 124 125def add_member_doc(symbol, value): 126 for key, c_type, py_type in member_methods: 127 if symbol.startswith(key): 128 method = symbol.replace(key, '') 129 if method == 'new': 130 return ('%feature("docstring") {} "{}\n\n' + 131 'See also gphoto2.{}"\n\n').format( 132 symbol, value, py_type) 133 return ('%feature("docstring") {} "{}\n\n' + 134 'See also gphoto2.{}.{}"\n\n' + 135 '%feature("docstring") {}::{} "{}\n\n' + 136 'See also gphoto2.{}"\n\n').format( 137 symbol, value, py_type, method, 138 c_type, method, value, symbol) 139 return '%feature("docstring") {} "{}"\n\n'.format(symbol, value) 140 141class build_doc(Command): 142 description = 'run doxygen to generate documentation' 143 user_options = [] 144 145 def initialize_options(self): 146 pass 147 148 def finalize_options(self): 149 pass 150 151 def run(self): 152 gp_versions = get_gp_versions() 153 self.announce('making docs for gphoto2 versions %s' % str(gp_versions), 2) 154 sys.path.append('doxy2swig') 155 from doxy2swig import Doxy2SWIG 156 for gp_version in gp_versions: 157 src_dir = 'libgphoto2-' + gp_version 158 os.chdir(src_dir) 159 self.spawn(['doxygen', '../developer/Doxyfile']) 160 os.chdir('..') 161 index_file = os.path.join(src_dir, 'doc', 'xml', 'index.xml') 162 self.announce('Doxy2SWIG ' + index_file, 2) 163 p = Doxy2SWIG(index_file, 164 with_function_signature = False, 165 with_type_info = False, 166 with_constructor_list = False, 167 with_attribute_list = False, 168 with_overloaded_functions = False, 169 textwidth = 72, 170 quiet = True) 171 p.generate() 172 text = ''.join(p.pieces) 173 with open(os.path.join('src', 'gphoto2', 'common', 174 'doc-' + gp_version + '.i'), 'w') as of: 175 for match in re.finditer('%feature\("docstring"\) (\w+) \"(.+?)\";', 176 text, re.DOTALL): 177 symbol = match.group(1) 178 value = match.group(2).strip() 179 if not value: 180 continue 181 of.write(add_member_doc(symbol, value)) 182 183cmdclass['build_doc'] = build_doc 184 185# add command to run SWIG 186class build_swig(Command): 187 description = 'run SWIG to regenerate interface files' 188 user_options = [] 189 190 def initialize_options(self): 191 pass 192 193 def finalize_options(self): 194 pass 195 196 def run(self): 197 # get list of modules (Python) and extensions (SWIG) 198 file_names = os.listdir(os.path.join('src', 'gphoto2')) 199 file_names.sort() 200 file_names = [os.path.splitext(x) for x in file_names] 201 ext_names = [x[0] for x in file_names if x[1] == '.i'] 202 # get gphoto2 versions to be swigged 203 gp_versions = get_gp_versions() 204 self.announce('swigging gphoto2 versions %s' % str(gp_versions), 2) 205 # do -builtin and not -builtin 206 swig_bis = [False] 207 cmd = ['swig', '-version'] 208 try: 209 swig_version = str(subprocess.check_output( 210 cmd, universal_newlines=True)) 211 except Exception: 212 error('ERROR: command "%s" failed', ' '.join(cmd)) 213 raise 214 for line in swig_version.split('\n'): 215 if 'Version' in line: 216 swig_version = tuple(map(int, line.split()[-1].split('.'))) 217 if swig_version != (2, 0, 11): 218 swig_bis.append(True) 219 break 220 for use_builtin in swig_bis: 221 # make options list 222 swig_opts = ['-python', '-nodefaultctor', '-O', '-Wextra', '-Werror'] 223 if use_builtin: 224 swig_opts += ['-builtin', '-nofastunpack'] 225 # do each gphoto2 version 226 for gp_version in gp_versions: 227 doc_file = os.path.join( 228 'src', 'gphoto2', 'common', 'doc-' + gp_version + '.i') 229 # do Python 2 and 3 230 for py_version in 2, 3: 231 output_dir = os.path.join('src', 'swig') 232 if use_builtin: 233 output_dir += '-bi' 234 output_dir += '-py' + str(py_version) 235 output_dir += '-gp' + gp_version 236 self.mkpath(output_dir) 237 version_opts = ['-outdir', output_dir] 238 if os.path.isfile(doc_file): 239 version_opts.append( 240 '-DDOC_FILE=' + os.path.basename(doc_file)) 241 inc_dir = 'libgphoto2-' + gp_version 242 if os.path.isdir(inc_dir): 243 version_opts.append('-I' + inc_dir) 244 version_opts.append( 245 '-I' + os.path.join(inc_dir, 'libgphoto2_port')) 246 else: 247 version_opts += gphoto2_include 248 if py_version >= 3: 249 version_opts.append('-py3') 250 # do each swig module 251 for ext_name in ext_names: 252 in_file = os.path.join('src', 'gphoto2', ext_name + '.i') 253 out_file = os.path.join(output_dir, ext_name + '_wrap.c') 254 self.spawn(['swig'] + swig_opts + version_opts + 255 ['-o', out_file, in_file]) 256 # create init module 257 init_file = os.path.join(output_dir, '__init__.py') 258 with open(init_file, 'w') as im: 259 im.write('__version__ = "{}"\n\n'.format(version)) 260 im.write(''' 261class GPhoto2Error(Exception): 262 """Exception raised by gphoto2 library errors 263 264 Attributes: 265 code (int): the gphoto2 error code 266 string (str): corresponding error message 267 """ 268 def __init__(self, code): 269 string = gp_result_as_string(code) 270 Exception.__init__(self, '[%d] %s' % (code, string)) 271 self.code = code 272 self.string = string 273 274''') 275 for name in ext_names: 276 im.write('from gphoto2.{} import *\n'.format(name)) 277 im.write(''' 278__all__ = dir() 279''') 280 # store SWIG version 281 info_file = os.path.join('src', 'info.txt') 282 with open(info_file, 'w') as info: 283 info.write('swig_version = {}\n'.format(repr(swig_version))) 284 285cmdclass['build_swig'] = build_swig 286 287# modify upload class to add appropriate git tag 288# requires GitPython - 'sudo pip install gitpython --pre' 289try: 290 import git 291 class upload(_upload): 292 def run(self): 293 message = 'v' + version + '\n\n' 294 with open('CHANGELOG.txt') as cl: 295 while not cl.readline().startswith('Changes'): 296 pass 297 while True: 298 line = cl.readline().strip() 299 if not line: 300 break 301 message += line + '\n' 302 repo = git.Repo() 303 tag = repo.create_tag('v' + version, message=message) 304 remote = repo.remotes.origin 305 remote.push(tags=True) 306 return _upload.run(self) 307 cmdclass['upload'] = upload 308except ImportError: 309 pass 310 311# set options for building distributions 312command_options['sdist'] = { 313 'formats' : ('setup.py', 'gztar'), 314 } 315 316# list example scripts 317examples = [os.path.join('examples', x) 318 for x in os.listdir('examples') if os.path.splitext(x)[1] == '.py'] 319 320with open('README.rst') as ldf: 321 long_description = ldf.read() 322 323setup(name = 'gphoto2', 324 version = version, 325 description = 'Python interface to libgphoto2', 326 long_description = long_description, 327 author = 'Jim Easterbrook', 328 author_email = 'jim@jim-easterbrook.me.uk', 329 url = 'https://github.com/jim-easterbrook/python-gphoto2', 330 classifiers = [ 331 'Development Status :: 5 - Production/Stable', 332 'Intended Audience :: Developers', 333 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 334 'Operating System :: MacOS', 335 'Operating System :: MacOS :: MacOS X', 336 'Operating System :: POSIX', 337 'Operating System :: POSIX :: BSD :: FreeBSD', 338 'Operating System :: POSIX :: BSD :: NetBSD', 339 'Operating System :: POSIX :: Linux', 340 'Programming Language :: Python :: 2', 341 'Programming Language :: Python :: 2.6', 342 'Programming Language :: Python :: 2.7', 343 'Programming Language :: Python :: 3', 344 'Topic :: Multimedia', 345 'Topic :: Multimedia :: Graphics', 346 'Topic :: Multimedia :: Graphics :: Capture', 347 ], 348 platforms = ['POSIX', 'MacOS'], 349 license = 'GNU GPL', 350 cmdclass = cmdclass, 351 command_options = command_options, 352 ext_package = 'gphoto2', 353 ext_modules = ext_modules, 354 packages = ['gphoto2'], 355 package_dir = {'gphoto2' : mod_src_dir}, 356 data_files = [ 357 ('share/examples/py38-gphoto2', examples), 358 ('share/doc/py38-gphoto2', [ 359 'CHANGELOG.txt', 'LICENSE.txt', 'README.rst']), 360 ], 361 ) 362