0fc21245e9e6c948ebe533d3389a3e6c90838390
[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                           "wc_adm.c")],
401             libraries=["svn_client-1", "svn_subr-1", "svn_ra-1", "svn_wc-1"]),
402         SvnExtension(
403             "subvertpy._ra",
404             [source_path(n) for n in ("_ra.c", "util.c", "editor.c")],
405             libraries=["svn_ra-1", "svn_delta-1", "svn_subr-1"]),
406         SvnExtension(
407             "subvertpy.repos", [source_path(n) for n in ("repos.c", "util.c")],
408             libraries=["svn_repos-1", "svn_subr-1", "svn_fs-1"]),
409         SvnExtension(
410             "subvertpy.wc",
411             [source_path(n) for n in
412                 ["wc.c", "wc_adm.c", "util.c", "editor.c"]],
413             libraries=["svn_wc-1", "svn_subr-1"]),
414         SvnExtension(
415             "subvertpy.subr",
416             [source_path(n)
417                 for n in ["util.c", "subr.c"]],
418             libraries=["svn_subr-1"]),
419         ]
420
421
422 subvertpy_version = (0, 11, 0)
423 subvertpy_version_string = ".".join(map(str, subvertpy_version))
424
425
426 if __name__ == "__main__":
427     setup(name='subvertpy',
428           description='Alternative Python bindings for Subversion',
429           keywords='svn subvertpy subversion bindings',
430           version=subvertpy_version_string,
431           url='https://jelmer.uk/subvertpy',
432           download_url="https://jelmer.uk/subvertpy/tarball/subvertpy-%s/" % (
433               subvertpy_version_string, ),
434           license='LGPLv2.1 or later',
435           author='Jelmer Vernooij',
436           author_email='jelmer@jelmer.uk',
437           long_description="""
438 Alternative Python bindings for Subversion. The goal is to have
439 complete, portable and "Pythonic" Python bindings.
440
441 Bindings are provided for the working copy, client, delta, remote access and
442 repository APIs. A hookable server side implementation of the custom Subversion
443 protocol (svn_ra) is also provided.
444
445 Differences with similar packages
446 ---------------------------------
447 subvertpy covers more of the APIs than python-svn. It provides a more
448 "Pythonic" API than python-subversion, which wraps the Subversion C API pretty
449 much directly. Neither provide a hookable server-side.
450
451 Dependencies
452 ------------
453 Subvertpy depends on Python 2.7 or 3.5, and Subversion 1.4 or later. It should
454 work on Windows as well as most POSIX-based platforms (including Linux, BSDs
455 and Mac OS X).
456 """,
457           packages=['subvertpy', 'subvertpy.tests'],
458           ext_modules=subvertpy_modules(),
459           scripts=['bin/subvertpy-fast-export'],
460           cmdclass=cmdclass,
461           classifiers=[
462               'Development Status :: 4 - Beta',
463               'License :: OSI Approved :: GNU General Public '
464               'License v2 or later (GPLv2+)',
465               'Programming Language :: Python :: 2.7',
466               'Programming Language :: Python :: 3.4',
467               'Programming Language :: Python :: 3.5',
468               'Programming Language :: Python :: 3.6',
469               'Programming Language :: Python :: Implementation :: CPython',
470               'Operating System :: POSIX',
471               'Topic :: Software Development :: Version Control',
472           ],
473           )