06540dd043e079db662cd068492558cd23cafcb5
[jelmer/subvertpy.git] / setup.py
1 #!/usr/bin/env python
2 # Setup file for subvertpy
3 # Copyright (C) 2005-2010 Jelmer Vernooij <jelmer@jelmer.uk>
4
5 from distutils.core import setup, Command
6 from distutils.extension import Extension
7 from distutils.command.build import build
8 from distutils import log
9 import sys
10 import os
11 import re
12 import subprocess
13
14
15 class CommandException(Exception):
16     """Encapsulate exit status of command execution"""
17     def __init__(self, msg, cmd, arg, status, val):
18         self.message = msg % (cmd, val)
19         Exception.__init__(self, self.message)
20         self.cmd = cmd
21         self.arg = arg
22         self.status = status
23
24     def not_found(self):
25         return os.WIFEXITED(self.status) and os.WEXITSTATUS(self.status) == 127
26
27
28 def split_shell_results(line):
29     return [s for s in line.split(" ") if s != ""]
30
31
32 def run_cmd(cmd, arg):
33     """Run specified command with given arguments, handling status"""
34     f = os.popen("'%s' %s" % (cmd, arg))
35     dir = f.read().rstrip("\n")
36     status = f.close()
37     if status is None:
38         return dir
39     if os.WIFEXITED(status):
40         code = os.WEXITSTATUS(status)
41         if code == 0:
42             return dir
43         raise CommandException("%s exited with status %d",
44                                cmd, arg, status, code)
45     if os.WIFSIGNALED(status):
46         signal = os.WTERMSIG(status)
47         raise CommandException("%s killed by signal %d",
48                                cmd, arg, status, signal)
49     raise CommandException("%s terminated abnormally (%d)",
50                            cmd, arg, status, status)
51
52
53 def config_value(command, arg):
54     cmds = [command] + [
55             os.path.join(p, command) for p in
56             ["/usr/local/apr/bin/", "/opt/local/bin/"]]
57     for cmd in cmds:
58         try:
59             return run_cmd(cmd, arg)
60         except CommandException as e:
61             if not e.not_found():
62                 raise
63     else:
64         raise Exception("apr-config not found."
65                         " Please set APR_CONFIG environment variable")
66
67
68 def apr_config(arg):
69     config_cmd = os.getenv("APR_CONFIG")
70     if config_cmd is None:
71         return config_value("apr-1-config", arg)
72     else:
73         return run_cmd(config_cmd, arg)
74
75
76 def apu_config(arg):
77     config_cmd = os.getenv("APU_CONFIG")
78     if config_cmd is None:
79         return config_value("apu-1-config", arg)
80     else:
81         return run_cmd(config_cmd, arg)
82
83
84 def apr_build_data():
85     """Determine the APR header file location."""
86     includedir = apr_config("--includedir")
87     if not os.path.isdir(includedir):
88         raise Exception("APR development headers not found")
89     extra_link_flags = apr_config("--link-ld --libs")
90     return (includedir, split_shell_results(extra_link_flags))
91
92
93 def apu_build_data():
94     """Determine the APR util header file location."""
95     includedir = apu_config("--includedir")
96     if not os.path.isdir(includedir):
97         raise Exception("APR util development headers not found")
98     extra_link_flags = apu_config("--link-ld --libs")
99     return (includedir, split_shell_results(extra_link_flags))
100
101
102 def svn_build_data():
103     """Determine the Subversion header file location."""
104     if "SVN_HEADER_PATH" in os.environ and "SVN_LIBRARY_PATH" in os.environ:
105         return ([os.getenv("SVN_HEADER_PATH")],
106                 [os.getenv("SVN_LIBRARY_PATH")], [], [])
107     svn_prefix = os.getenv("SVN_PREFIX")
108     if svn_prefix is None:
109         basedirs = ["/usr/local", "/usr"]
110         for basedir in basedirs:
111             includedir = os.path.join(basedir, "include/subversion-1")
112             if os.path.isdir(includedir):
113                 svn_prefix = basedir
114                 break
115     if svn_prefix is not None:
116         return ([os.path.join(svn_prefix, "include/subversion-1")],
117                 [os.path.join(svn_prefix, "lib")], [], [])
118     raise Exception("Subversion development files not found. "
119                     "Please set SVN_PREFIX or (SVN_LIBRARY_PATH and "
120                     "SVN_HEADER_PATH) environment variable. ")
121
122
123 def is_keychain_provider_available():
124     """
125     Checks for the availability of the Keychain simple authentication provider
126     in Subversion by compiling a simple test program.
127     """
128     abd = apr_build_data()
129     sbd = svn_build_data()
130     gcc_command_args = (
131             ['gcc'] + ['-I' + inc for inc in sbd[0]] +
132             ['-L' + lib for lib in sbd[1]] +
133             ['-I' + abd[0], '-lsvn_subr-1', '-x', 'c', '-'])
134     gcc = subprocess.Popen(
135             gcc_command_args,
136             stdin=subprocess.PIPE, universal_newlines=True,
137             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
138     gcc.communicate("""
139 #include <svn_auth.h>
140 int main(int argc, const char* arv[]) {
141     svn_auth_get_keychain_simple_provider(NULL, NULL);
142 }
143 """)
144     return (gcc.returncode == 0)
145
146
147 class VersionQuery(object):
148
149     def __init__(self, filename):
150         self.filename = filename
151         f = open(filename, "rU")
152         try:
153             self.text = f.read()
154         finally:
155             f.close()
156
157     def grep(self, what):
158         m = re.search(r"^#define\s+%s\s+(\d+)\s*$" % (what,), self.text,
159                       re.MULTILINE)
160         if not m:
161             raise Exception(
162                     "Definition for %s was not found in file %s." %
163                     (what, self.filename))
164         return int(m.group(1))
165
166
167 # Windows versions - we use environment variables to locate the directories
168 # and hard-code a list of libraries.
169 if os.name == "nt":
170     def get_apr_version():
171         apr_version_file = os.path.join(
172                 os.environ["SVN_DEV"],
173                 r"include\apr\apr_version.h")
174         if not os.path.isfile(apr_version_file):
175             raise Exception(
176                 "Please check that your SVN_DEV location is correct.\n"
177                 "Unable to find required apr\\apr_version.h file.")
178         query = VersionQuery(apr_version_file)
179         return (query.grep("APR_MAJOR_VERSION"),
180                 query.grep("APR_MINOR_VERSION"),
181                 query.grep("APR_PATCH_VERSION"))
182
183     def get_svn_version():
184         svn_version_file = os.path.join(
185                 os.environ["SVN_DEV"],
186                 r"include\svn_version.h")
187         if not os.path.isfile(svn_version_file):
188             raise Exception(
189                 "Please check that your SVN_DEV location is correct.\n"
190                 "Unable to find required svn_version.h file.")
191         query = VersionQuery(svn_version_file)
192         return (query.grep("SVN_VER_MAJOR"),
193                 query.grep("SVN_VER_MINOR"),
194                 query.grep("SVN_VER_PATCH"))
195
196     # just clobber the functions above we can't use
197     # for simplicitly, everything is done in the 'svn' one
198
199     def apr_build_data():  # noqa: F811
200         return '.', []
201
202     def apu_build_data():  # noqa: F811
203         return '.', []
204
205     def svn_build_data():  # noqa: F811
206         # environment vars for the directories we need.
207         svn_dev_dir = os.environ.get("SVN_DEV")
208         if not svn_dev_dir or not os.path.isdir(svn_dev_dir):
209             raise Exception(
210                 "Please set SVN_DEV to the location of the svn development "
211                 "packages.\nThese can be downloaded from:\n"
212                 "http://sourceforge.net/projects/win32svn/files/")
213         svn_bdb_dir = os.environ.get("SVN_BDB")
214         if not svn_bdb_dir or not os.path.isdir(svn_bdb_dir):
215             raise Exception(
216                 "Please set SVN_BDB to the location of the svn BDB packages "
217                 "- see README.txt in the SVN_DEV dir")
218         svn_libintl_dir = os.environ.get("SVN_LIBINTL")
219         if not svn_libintl_dir or not os.path.isdir(svn_libintl_dir):
220             raise Exception(
221                 "Please set SVN_LIBINTL to the location of the svn libintl "
222                 "packages - see README.txt in the SVN_DEV dir")
223
224         svn_version = get_svn_version()
225         apr_version = get_apr_version()
226
227         includes = [
228             # apr dirs.
229             os.path.join(svn_dev_dir, r"include\apr"),
230             os.path.join(svn_dev_dir, r"include\apr-util"),
231             os.path.join(svn_dev_dir, r"include\apr-iconv"),
232             # svn dirs.
233             os.path.join(svn_dev_dir, "include"),
234         ]
235         lib_dirs = [
236             os.path.join(svn_dev_dir, "lib"),
237             os.path.join(svn_dev_dir, "lib", "apr"),
238             os.path.join(svn_dev_dir, "lib", "apr-iconv"),
239             os.path.join(svn_dev_dir, "lib", "apr-util"),
240             os.path.join(svn_dev_dir, "lib", "neon"),
241             os.path.join(svn_bdb_dir, "lib"),
242             os.path.join(svn_libintl_dir, "lib"),
243         ]
244         aprlibs = """libapr libapriconv libaprutil""".split()
245         if apr_version[0] == 1:
246             aprlibs = [aprlib + "-1" for aprlib in aprlibs]
247         elif apr_version[0] > 1:
248             raise Exception(
249                 "You have apr version %d.%d.%d.\n"
250                 "This setup only knows how to build with 0.*.* or 1.*.*." %
251                 apr_version)
252         libs = """libneon libsvn_subr-1 libsvn_client-1 libsvn_ra-1
253                   libsvn_ra_dav-1 libsvn_ra_local-1 libsvn_ra_svn-1
254                   libsvn_repos-1 libsvn_wc-1 libsvn_delta-1 libsvn_diff-1
255                   libsvn_fs-1 libsvn_repos-1 libsvn_fs_fs-1 libsvn_fs_base-1
256                   intl3_svn
257                   xml
258                   advapi32 shell32 ws2_32 zlibstat
259                """.split()
260         if svn_version >= (1, 7, 0):
261             libs += ["libdb48"]
262         else:
263             libs += ["libdb44"]
264         if svn_version >= (1, 5, 0):
265             # Since 1.5.0 libsvn_ra_dav-1 was removed
266             libs.remove("libsvn_ra_dav-1")
267
268         return includes, lib_dirs, [], aprlibs+libs,
269
270 (apr_includedir, apr_link_flags) = apr_build_data()
271 (apu_includedir, apu_link_flags) = apu_build_data()
272 (svn_includedirs, svn_libdirs, svn_link_flags, extra_libs) = svn_build_data()
273
274
275 class SvnExtension(Extension):
276
277     def __init__(self, name, *args, **kwargs):
278         kwargs["include_dirs"] = ([apr_includedir, apu_includedir] +
279                                   svn_includedirs + ["subvertpy"])
280         kwargs["library_dirs"] = svn_libdirs
281         # Note that the apr-util link flags are not included here, as
282         # subvertpy only uses some apr util constants but does not use
283         # the library directly.
284         kwargs["extra_link_args"] = apr_link_flags + svn_link_flags
285         if os.name == 'nt':
286             # on windows, just ignore and overwrite the libraries!
287             kwargs["libraries"] = extra_libs
288             # APR needs WIN32 defined.
289             kwargs["define_macros"] = [("WIN32", None)]
290         if sys.platform == 'darwin':
291             # on Mac OS X, we need to check for Keychain availability
292             if is_keychain_provider_available():
293                 if "define_macros" not in kwargs:
294                     kwargs["define_macros"] = []
295                 kwargs["define_macros"].extend((
296                     ('DARWIN', None),
297                     ('SVN_KEYCHAIN_PROVIDER_AVAILABLE', '1'))
298                     )
299         Extension.__init__(self, name, *args, **kwargs)
300
301
302 class TestCommand(Command):
303     """Command for running unittests without install."""
304
305     user_options = [
306         ("args=", None, '''The command args string passed to '''
307                         '''unittest framework, such as --args="-v -f"''')]
308
309     def initialize_options(self):
310         self.args = ''
311         pass
312
313     def finalize_options(self):
314         pass
315
316     def run(self):
317         self.run_command('build')
318         bld = self.distribution.get_command_obj('build')
319         # Add build_lib in to sys.path so that unittest can found DLLs and libs
320         sys.path = [os.path.abspath(bld.build_lib)] + sys.path
321         os.chdir(bld.build_lib)
322         log.info("Running unittest without install.")
323
324         import shlex
325         import unittest
326         test_argv0 = [sys.argv[0] + ' test --args=']
327         # For transfering args to unittest, we have to split args
328         # by ourself, so that command like:
329         # python setup.py test --args="-v -f"
330         # can be executed, and the parameter '-v -f' can be
331         # transfering to unittest properly.
332         test_argv = test_argv0 + shlex.split(self.args)
333         unittest.main(module=None, defaultTest='subvertpy.tests.test_suite',
334                       argv=test_argv)
335
336
337 class BuildWithDLLs(build):
338     def _get_dlls(self):
339         # return a list of of (FQ-in-name, relative-out-name) tuples.
340         ret = []
341         # the apr binaries.
342         apr_bins = [libname + ".dll" for libname in extra_libs
343                     if libname.startswith("libapr")]
344         if get_svn_version() >= (1, 5, 0):
345             # Since 1.5.0 these libraries became shared
346             apr_bins += """libsvn_client-1.dll libsvn_delta-1.dll libsvn_diff-1.dll
347                            libsvn_fs-1.dll libsvn_ra-1.dll libsvn_repos-1.dll
348                            libsvn_subr-1.dll libsvn_wc-1.dll libsasl.dll
349                            """.split()
350         if get_svn_version() >= (1, 7, 0):
351             apr_bins += ["libdb48.dll"]
352         else:
353             apr_bins += ["libdb44.dll"]
354         apr_bins += """intl3_svn.dll libeay32.dll ssleay32.dll""".split()
355         look_dirs = os.environ.get("PATH", "").split(os.pathsep)
356         look_dirs.insert(0, os.path.join(os.environ["SVN_DEV"], "bin"))
357
358         target = os.path.abspath(os.path.join(self.build_lib, 'subvertpy'))
359         for bin in apr_bins:
360             for look in look_dirs:
361                 f = os.path.join(look, bin)
362                 if os.path.isfile(f):
363                     ret.append((f, target))
364                     break
365             else:
366                 log.warn("Could not find required DLL %r to include", bin)
367                 log.debug("(looked in %s)", look_dirs)
368         return ret
369
370     def run(self):
371         build.run(self)
372         # the apr binaries.
373         # On Windows we package up the apr dlls with the plugin.
374         for s, d in self._get_dlls():
375             self.copy_file(s, d)
376
377     def get_outputs(self):
378         ret = build.get_outputs(self)
379         ret.extend(info[1] for info in self._get_dlls())
380         return ret
381
382
383 cmdclass = {'test': TestCommand}
384 if os.name == 'nt':
385     # BuildWithDLLs can copy external DLLs into build directory On Win32.
386     # So we can running unittest directly from build directory.
387     cmdclass['build'] = BuildWithDLLs
388
389
390 def source_path(filename):
391     return os.path.join("subvertpy", filename)
392
393
394 def subvertpy_modules():
395     return [
396         SvnExtension(
397             "subvertpy.client",
398             [source_path(n)
399                 for n in ("client.c", "editor.c", "util.c", "_ra.c", "wc.c")],
400             libraries=["svn_client-1", "svn_subr-1", "svn_ra-1", "svn_wc-1"]),
401         SvnExtension(
402             "subvertpy._ra",
403             [source_path(n) for n in ("_ra.c", "util.c", "editor.c")],
404             libraries=["svn_ra-1", "svn_delta-1", "svn_subr-1"]),
405         SvnExtension(
406             "subvertpy.repos", [source_path(n) for n in ("repos.c", "util.c")],
407             libraries=["svn_repos-1", "svn_subr-1", "svn_fs-1"]),
408         SvnExtension(
409             "subvertpy.wc",
410             [source_path(n) for n in ("wc.c", "util.c", "editor.c")],
411             libraries=["svn_wc-1", "svn_subr-1"])
412         ]
413
414
415 subvertpy_version = (0, 10, 0)
416 subvertpy_version_string = ".".join(map(str, subvertpy_version))
417
418
419 if __name__ == "__main__":
420     setup(name='subvertpy',
421           description='Alternative Python bindings for Subversion',
422           keywords='svn subvertpy subversion bindings',
423           version=subvertpy_version_string,
424           url='https://jelmer.uk/subvertpy',
425           download_url="https://jelmer.uk/subvertpy/subvertpy-%s.tar.gz" % (
426               subvertpy_version_string, ),
427           license='LGPLv2.1 or later',
428           author='Jelmer Vernooij',
429           author_email='jelmer@jelmer.uk',
430           long_description="""
431           Alternative Python bindings for Subversion. The goal is to have
432           complete, portable and "Pythonic" Python bindings.
433           """,
434           packages=['subvertpy', 'subvertpy.tests'],
435           ext_modules=subvertpy_modules(),
436           scripts=['bin/subvertpy-fast-export'],
437           cmdclass=cmdclass,
438           classifiers=[
439               'Development Status :: 4 - Beta',
440               'License :: OSI Approved :: GNU General Public '
441               'License v2 or later (GPLv2+)',
442               'Programming Language :: Python :: 2.7',
443               'Programming Language :: Python :: 3.4',
444               'Programming Language :: Python :: 3.5',
445               'Programming Language :: Python :: 3.6',
446               'Programming Language :: Python :: Implementation :: CPython',
447               'Operating System :: POSIX',
448               'Topic :: Software Development :: Version Control',
449           ],
450           )