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