1#!/usr/bin/env python
2#    Licensed to the Apache Software Foundation (ASF) under one
3#    or more contributor license agreements.  See the NOTICE file
4#    distributed with this work for additional information
5#    regarding copyright ownership.  The ASF licenses this file
6#    to you under the Apache License, Version 2.0 (the
7#    "License"); you may not use this file except in compliance
8#    with the License.  You may obtain a copy of the License at
9#
10#      http://www.apache.org/licenses/LICENSE-2.0
11#
12#    Unless required by applicable law or agreed to in writing,
13#    software distributed under the License is distributed on an
14#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15#    KIND, either express or implied.  See the License for the
16#    specific language governing permissions and limitations
17#    under the License.
18
19import errno, os, re, sys, tempfile
20
21from distutils import log
22from distutils.cmd import Command
23from distutils.command.build import build as _build
24from distutils.command.clean import clean as _clean
25from distutils.core import setup
26from distutils.dir_util import remove_tree
27from distutils.errors import DistutilsExecError
28from distutils.errors import DistutilsOptionError
29
30from glob import glob
31
32class clean(_clean):
33  """Special distutils command for cleaning the Subversion ctypes bindings."""
34  description = "clean the Subversion ctypes Python bindings"
35
36  def initialize_options (self):
37    _clean.initialize_options(self)
38
39  # initialize_options()
40
41  def finalize_options (self):
42    _clean.finalize_options(self)
43
44  # finalize_options()
45
46  def run(self):
47    functions_py = os.path.join(os.path.dirname(__file__), "csvn", "core",
48                                "functions.py")
49    functions_pyc = os.path.join(os.path.dirname(__file__), "csvn", "core",
50                                 "functions.pyc")
51    svn_all_py = os.path.join(os.path.dirname(__file__), "svn_all.py")
52    svn_all2_py = os.path.join(os.path.dirname(__file__), "svn_all2.py")
53
54    for f in (functions_py, functions_pyc, svn_all_py, svn_all2_py):
55      if os.path.exists(f):
56        log.info("removing '%s'", os.path.normpath(f))
57
58        if not self.dry_run:
59          os.remove(f)
60      else:
61        log.debug("'%s' does not exist -- can't clean it", os.path.normpath(f))
62
63    # Run standard clean command
64    _clean.run(self)
65
66  # run()
67
68# class clean
69
70class build(_build):
71  """Special distutils command for building the Subversion ctypes bindings."""
72
73  description = "build the Subversion ctypes Python bindings"
74
75  _build.user_options.append(("apr=", None, "full path to where apr is "
76                              "installed or the full path to the apr-1-config or "
77                              "apr-config file"))
78  _build.user_options.append(("apr-util=", None, "full path to where apr-util "
79                              "is installed or the full path to the apu-1-config or "
80                              "apu-config file"))
81  _build.user_options.append(("subversion=", None, "full path to where "
82                              "Subversion is installed"))
83  _build.user_options.append(("svn-headers=", None, "Full path to the "
84                              "Subversion header files, if they are not in a "
85                              "standard location"))
86  _build.user_options.append(("ctypesgen=", None, "full path to where ctypesgen "
87                              "is installed, to the ctypesgen source tree or "
88                              "the full path to ctypesgen.py"))
89  _build.user_options.append(("cppflags=", None, "extra flags to pass to the c "
90                              "preprocessor"))
91  _build.user_options.append(("ldflags=", None, "extra flags to pass to the "
92                              "ctypesgen linker"))
93  _build.user_options.append(("lib-dirs=", None, "colon-delimited list of paths "
94                              "to append to the search path"))
95  _build.user_options.append(("save-preprocessed-headers=", None, "full path to "
96                              "where to save the preprocessed headers"))
97
98  def initialize_options (self):
99    _build.initialize_options(self)
100    self.apr = None
101    self.apr_util = None
102    self.ctypesgen = None
103    self.subversion = None
104    self.svn_headers = None
105    self.cppflags = ""
106    self.ldflags = ""
107    self.lib_dirs = None
108    self.save_preprocessed_headers = None
109
110  # initialize_options()
111
112  def finalize_options (self):
113    _build.finalize_options(self)
114
115    # Distutils doesn't appear to like when you have --dry-run after the build
116    # build command so fail out if this is the case.
117    if self.dry_run != self.distribution.dry_run:
118      raise DistutilsOptionError("The --dry-run flag must be specified " \
119                                 "before the 'build' command")
120
121  # finalize_options()
122
123  ##############################################################################
124  # Get build configuration
125  ##############################################################################
126  def get_build_config (self):
127    flags = []
128    ldflags = []
129    library_path = []
130    ferr = None
131    apr_include_dir = None
132
133    fout = self.run_cmd("%s --includes --cppflags" % self.apr_config)
134    if fout:
135      flags = fout.split()
136      apr_prefix = self.run_cmd("%s --prefix" % self.apr_config)
137      apr_prefix = apr_prefix.strip()
138      apr_include_dir = self.run_cmd("%s --includedir" % self.apr_config).strip()
139      apr_version = self.run_cmd("%s --version" % self.apr_config).strip()
140      cpp  = self.run_cmd("%s --cpp" % self.apr_config).strip()
141      fout = self.run_cmd("%s --ldflags --link-ld" % self.apr_config)
142      if fout:
143        ldflags = fout.split()
144      else:
145        log.error(ferr)
146        raise DistutilsExecError("Problem running '%s'.  Check the output " \
147                                 "for details" % self.apr_config)
148
149      fout = self.run_cmd("%s --includes" % self.apu_config)
150      if fout:
151        flags += fout.split()
152        fout = self.run_cmd("%s --ldflags --link-ld" % self.apu_config)
153        if fout:
154          ldflags += fout.split()
155        else:
156          log.error(ferr)
157          raise DistutilsExecError("Problem running '%s'.  Check the output " \
158                                   "for details" % self.apr_config)
159
160        subversion_prefixes = [
161          self.subversion,
162          "/usr/local",
163          "/usr"
164          ]
165
166        if self.subversion != "/usr":
167          ldflags.append("-L%s/lib" % self.subversion)
168        if self.svn_include_dir[-18:] == "subversion/include":
169          flags.append("-Isubversion/include")
170        else:
171          flags.append("-I%s" % self.svn_include_dir)
172
173        # List the libraries in the order they should be loaded
174        libraries = [
175          "svn_subr-1",
176          "svn_diff-1",
177          "svn_delta-1",
178          "svn_fs-1",
179          "svn_repos-1",
180          "svn_wc-1",
181          "svn_ra-1",
182          "svn_client-1",
183          ]
184
185        for lib in libraries:
186          ldflags.append("-l%s" % lib)
187
188        if apr_prefix != '/usr':
189          library_path.append("%s/lib" % apr_prefix)
190          if self.subversion != '/usr' and self.subversion != apr_prefix:
191            library_path.append("%s/lib" % self.subversion)
192
193        return (apr_prefix, apr_include_dir, cpp + " " + self.cppflags,
194                " ".join(ldflags) + " " + self.ldflags, " ".join(flags),
195                ":".join(library_path))
196
197  # get_build_config()
198
199  ##############################################################################
200  # Build csvn/core/functions.py
201  ##############################################################################
202  def build_functions_py(self):
203    (apr_prefix, apr_include_dir, cpp, ldflags, flags,
204     library_path) = self.get_build_config()
205    cwd = os.getcwd()
206    if self.svn_include_dir[-18:] == "subversion/include":
207      includes = ('subversion/include/svn_*.h '
208                  '%s/ap[ru]_*.h' % apr_include_dir)
209      cmd = ["%s %s --cpp '%s %s' %s "
210             "%s -o subversion/bindings/ctypes-python/svn_all.py "
211             "--no-macro-warnings --strip-build-path=%s" % (sys.executable,
212                                                            self.ctypesgen_py, cpp,
213                                                            flags, ldflags,
214                                                            includes, self.svn_include_dir[:-19])]
215      os.chdir(self.svn_include_dir[:-19])
216    else:
217      includes = ('%s/svn_*.h '
218                  '%s/ap[ru]_*.h' % (self.svn_include_dir, apr_include_dir))
219      cmd = ["%s %s --cpp '%s %s' %s "
220             "%s -o svn_all.py --no-macro-warnings" % (sys.executable,
221                                                       self.ctypesgen_py, cpp,
222                                                       flags, ldflags,
223                                                       includes)]
224    if self.lib_dirs:
225      cmd.extend('-R ' + x for x in self.lib_dirs.split(":"))
226    cmd = ' '.join(cmd)
227
228    if self.save_preprocessed_headers:
229      cmd += " --save-preprocessed-headers=%s" % \
230          os.path.abspath(self.save_preprocessed_headers)
231
232    if self.verbose or self.dry_run:
233      status = self.execute(os.system, (cmd,), cmd)
234    else:
235      f = os.popen(cmd, 'r')
236      f.read() # Required to avoid the 'Broken pipe' error.
237      status = f.close() # None is returned for the usual 0 return code
238
239    os.chdir(cwd)
240
241    if os.name == "posix" and status and status != 0:
242      if os.WIFEXITED(status):
243        status = os.WEXITSTATUS(status)
244        if status != 0:
245          sys.exit(status)
246        elif os.WIFSIGNALED(status):
247          log.error("ctypesgen.py killed with signal %d" % os.WTERMSIG(status))
248          sys.exit(2)
249        elif os.WIFSTOPPED(status):
250          log.error("ctypesgen.py stopped with signal %d" % os.WSTOPSIG(status))
251          sys.exit(2)
252        else:
253          log.error("ctypesgen.py exited with invalid status %d", status)
254          sys.exit(2)
255
256    if not self.dry_run:
257      r = re.compile(r"(\s+\w+)\.restype = POINTER\(svn_error_t\)")
258      out = open("svn_all2.py", "w")
259      for line in open("svn_all.py"):
260        line = r.sub("\\1.restype = POINTER(svn_error_t)\n\\1.errcheck = _svn_errcheck", line)
261
262        if not line.startswith("FILE ="):
263          out.write(line)
264      out.close()
265
266    cmd = "cat csvn/core/functions.py.in svn_all2.py > csvn/core/functions.py"
267    self.execute(os.system, (cmd,), cmd)
268
269    log.info("Generated csvn/core/functions.py successfully")
270
271  # build_functions_py()
272
273  def run_cmd(self, cmd):
274    return os.popen(cmd).read()
275
276  # run_cmd()
277
278  def validate_options(self):
279    # Validate apr
280    if not self.apr:
281      self.apr = find_in_path('apr-1-config')
282
283      if not self.apr:
284        self.apr = find_in_path('apr-config')
285
286      if self.apr:
287        log.info("Found %s" % self.apr)
288      else:
289        raise DistutilsOptionError("Could not find apr-1-config or " \
290                                   "apr-config.  Please rerun with the " \
291                                   "--apr option.")
292
293    if os.path.exists(self.apr):
294      if os.path.isdir(self.apr):
295        if os.path.exists(os.path.join(self.apr, "bin", "apr-1-config")):
296          self.apr_config = os.path.join(self.apr, "bin", "apr-1-config")
297        elif os.path.exists(os.path.join(self.apr, "bin", "apr-config")):
298          self.apr_config = os.path.join(self.apr, "bin", "apr-config")
299        else:
300          self.apr_config = None
301      elif os.path.basename(self.apr) in ("apr-1-config", "apr-config"):
302        self.apr_config = self.apr
303      else:
304        self.apr_config = None
305    else:
306      self.apr_config = None
307
308    if not self.apr_config:
309      raise DistutilsOptionError("The --apr option is not valid.  It must " \
310                                 "point to a valid apr installation or " \
311                                 "to either the apr-1-config file or the " \
312                                 "apr-config file")
313
314    # Validate apr-util
315    if not self.apr_util:
316      self.apr_util = find_in_path('apu-1-config')
317
318      if not self.apr_util:
319        self.apr_util = find_in_path('apu-config')
320
321      if self.apr_util:
322        log.info("Found %s" % self.apr_util)
323      else:
324        raise DistutilsOptionError("Could not find apu-1-config or " \
325                                   "apu-config.  Please rerun with the " \
326                                   "--apr-util option.")
327
328    if os.path.exists(self.apr_util):
329      if os.path.isdir(self.apr_util):
330        if os.path.exists(os.path.join(self.apr_util, "bin", "apu-1-config")):
331          self.apu_config = os.path.join(self.apr_util, "bin", "apu-1-config")
332        elif os.path.exists(os.path.join(self.apr_util, "bin", "apu-config")):
333          self.apu_config = os.path.join(self.apr_util, "bin", "apu-config")
334        else:
335          self.apu_config = None
336      elif os.path.basename(self.apr_util) in ("apu-1-config", "apu-config"):
337        self.apu_config = self.apr_util
338      else:
339        self.apu_config = None
340    else:
341      self.apu_config = None
342
343    if not self.apu_config:
344      raise DistutilsOptionError("The --apr-util option is not valid.  It " \
345                                 "must point to a valid apr-util " \
346                                 "installation or to either the apu-1-config " \
347                                 "file or the apu-config file")
348
349    # Validate subversion
350    if not self.subversion:
351      self.subversion = find_in_path('svn')
352
353      if self.subversion:
354        log.info("Found %s" % self.subversion)
355        # Get the installation root instead of path to 'svn'
356        self.subversion = os.path.normpath(os.path.join(self.subversion, "..",
357                                                        ".."))
358      else:
359        raise DistutilsOptionError("Could not find Subversion.  Please rerun " \
360                                   "with the --subversion option.")
361
362    # Validate svn-headers, if present
363    if self.svn_headers:
364      if os.path.isdir(self.svn_headers):
365        if os.path.exists(os.path.join(self.svn_headers, "svn_client.h")):
366          self.svn_include_dir = self.svn_headers
367        elif os.path.exists(os.path.join(self.svn_headers, "subversion-1",
368                                         "svn_client.h")):
369          self.svn_include_dir = os.path.join(self.svn_headers, "subversion-1")
370        else:
371          self.svn_include_dir = None
372      else:
373        self.svn_include_dir = None
374    elif os.path.exists(os.path.join(self.subversion, "include",
375                                     "subversion-1")):
376      self.svn_include_dir = "%s/include/subversion-1" % self.subversion
377    else:
378      self.svn_include_dir = None
379
380    if not self.svn_include_dir:
381      msg = ""
382
383      if self.svn_headers:
384        msg = "The --svn-headers options is not valid.  It must point to " \
385              "either a Subversion include directory or the Subversion " \
386              "include/subversion-1 directory."
387      else:
388        msg = "The --subversion option is not valid. " \
389              "Could not locate %s/include/" \
390              "subversion-1/svn_client.h" % self.subversion
391
392      raise DistutilsOptionError(msg)
393
394    # Validate ctypesgen
395    if not self.ctypesgen:
396      self.ctypesgen = find_in_path('ctypesgen.py')
397
398      if self.ctypesgen:
399        log.info("Found %s" % self.ctypesgen)
400      else:
401        raise DistutilsOptionError("Could not find ctypesgen.  Please rerun " \
402                                   "with the --ctypesgen option.")
403
404    if os.path.exists(self.ctypesgen):
405      if os.path.isdir(self.ctypesgen):
406        if os.path.exists(os.path.join(self.ctypesgen, "ctypesgen.py")):
407          self.ctypesgen_py = os.path.join(self.ctypesgen, "ctypesgen.py")
408        elif os.path.exists(os.path.join(self.ctypesgen, "bin",
409                                         "ctypesgen.py")):
410          self.ctypesgen_py = os.path.join(self.ctypesgen, "bin",
411                                           "ctypesgen.py")
412        else:
413          self.ctypesgen_py = None
414      elif os.path.basename(self.ctypesgen) == "ctypesgen.py":
415          self.ctypesgen_py = self.ctypesgen
416      else:
417        self.ctypesgen_py = None
418    else:
419      self.ctypesgen_py = None
420
421    if not self.ctypesgen_py:
422      raise DistutilsOptionError("The --ctypesgen option is not valid.  It " \
423                                 "must point to a valid ctypesgen " \
424                                 "installation, a ctypesgen source tree or " \
425                                 "to the ctypesgen.py script")
426
427  # validate_functions()
428
429  def run (self):
430    # We only want to build if functions.py is not present.
431    if not os.path.exists(os.path.join(os.path.dirname(__file__),
432                                       "csvn", "core",
433                                       "functions.py")):
434      if 'build' not in self.distribution.commands:
435        raise DistutilsOptionError("You must run 'build' explicitly before " \
436                                   "you can proceed")
437
438      # Validate the command line options
439      self.validate_options()
440
441      # Generate functions.py
442      self.build_functions_py()
443    else:
444      log.info("csvn/core/functions.py was not regenerated (output up-to-date)")
445
446    # Run the standard build command.
447    _build.run(self)
448
449  # run()
450
451# class build
452
453def find_in_path(file):
454  paths = []
455  if os.environ.has_key('PATH'):
456    paths = os.environ['PATH'].split(os.pathsep)
457
458  for path in paths:
459    file_path = os.path.join(path, file)
460
461    if os.path.exists(file_path):
462      # Return the path to the first existing file found in PATH
463      return os.path.abspath(file_path)
464
465  return None
466# find_in_path()
467
468setup(cmdclass={'build': build, 'clean': clean},
469      name='svn-ctypes-python-bindings',
470      version='0.1',
471      description='Python bindings for the Subversion version control system.',
472      author='The Subversion Team',
473      author_email='dev@subversion.apache.org',
474      url='http://subversion.apache.org',
475      packages=['csvn', 'csvn.core', 'csvn.ext'],
476      license='Apache License, Version 2.0',
477     )
478
479# TODO: We need to create our own bdist_rpm implementation so that we can pass
480#       our required arguments to the build command being called by bdist_rpm.
481