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