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