setup.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. # -*- encoding: utf-8 *-*
  2. import os
  3. import re
  4. import sys
  5. import subprocess
  6. from glob import glob
  7. from distutils.command.build import build
  8. from distutils.core import Command
  9. import textwrap
  10. min_python = (3, 4)
  11. my_python = sys.version_info
  12. if my_python < min_python:
  13. print("Borg requires Python %d.%d or later" % min_python)
  14. sys.exit(1)
  15. # Are we building on ReadTheDocs?
  16. on_rtd = os.environ.get('READTHEDOCS')
  17. # msgpack pure python data corruption was fixed in 0.4.6.
  18. # Also, we might use some rather recent API features.
  19. install_requires = ['msgpack-python>=0.4.6', ]
  20. extras_require = {
  21. # llfuse 0.40 (tested, proven, ok), needs FUSE version >= 2.8.0
  22. # llfuse 0.41 (tested shortly, looks ok), needs FUSE version >= 2.8.0
  23. # llfuse 0.42 (tested shortly, looks ok), needs FUSE version >= 2.8.0
  24. # llfuse 1.0 (tested shortly, looks ok), needs FUSE version >= 2.8.0
  25. # llfuse 2.0 will break API
  26. 'fuse': ['llfuse<2.0', ],
  27. }
  28. from setuptools import setup, Extension
  29. from setuptools.command.sdist import sdist
  30. compress_source = 'borg/compress.pyx'
  31. crypto_source = 'borg/crypto.pyx'
  32. chunker_source = 'borg/chunker.pyx'
  33. hashindex_source = 'borg/hashindex.pyx'
  34. platform_linux_source = 'borg/platform_linux.pyx'
  35. platform_darwin_source = 'borg/platform_darwin.pyx'
  36. platform_freebsd_source = 'borg/platform_freebsd.pyx'
  37. try:
  38. from Cython.Distutils import build_ext
  39. import Cython.Compiler.Main as cython_compiler
  40. class Sdist(sdist):
  41. def __init__(self, *args, **kwargs):
  42. for src in glob('borg/*.pyx'):
  43. cython_compiler.compile(src, cython_compiler.default_options)
  44. super().__init__(*args, **kwargs)
  45. def make_distribution(self):
  46. self.filelist.extend([
  47. 'borg/compress.c',
  48. 'borg/crypto.c',
  49. 'borg/chunker.c', 'borg/_chunker.c',
  50. 'borg/hashindex.c', 'borg/_hashindex.c',
  51. 'borg/platform_linux.c',
  52. 'borg/platform_freebsd.c',
  53. 'borg/platform_darwin.c',
  54. ])
  55. super().make_distribution()
  56. except ImportError:
  57. class Sdist(sdist):
  58. def __init__(self, *args, **kwargs):
  59. raise Exception('Cython is required to run sdist')
  60. compress_source = compress_source.replace('.pyx', '.c')
  61. crypto_source = crypto_source.replace('.pyx', '.c')
  62. chunker_source = chunker_source.replace('.pyx', '.c')
  63. hashindex_source = hashindex_source.replace('.pyx', '.c')
  64. platform_linux_source = platform_linux_source.replace('.pyx', '.c')
  65. platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c')
  66. platform_darwin_source = platform_darwin_source.replace('.pyx', '.c')
  67. from distutils.command.build_ext import build_ext
  68. if not on_rtd and not all(os.path.exists(path) for path in [
  69. compress_source, crypto_source, chunker_source, hashindex_source,
  70. platform_linux_source, platform_freebsd_source]):
  71. raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.')
  72. def detect_openssl(prefixes):
  73. for prefix in prefixes:
  74. filename = os.path.join(prefix, 'include', 'openssl', 'evp.h')
  75. if os.path.exists(filename):
  76. with open(filename, 'r') as fd:
  77. if 'PKCS5_PBKDF2_HMAC(' in fd.read():
  78. return prefix
  79. def detect_lz4(prefixes):
  80. for prefix in prefixes:
  81. filename = os.path.join(prefix, 'include', 'lz4.h')
  82. if os.path.exists(filename):
  83. with open(filename, 'r') as fd:
  84. if 'LZ4_decompress_safe' in fd.read():
  85. return prefix
  86. include_dirs = []
  87. library_dirs = []
  88. windowsIncludeDirs = []
  89. if sys.platform == 'win32':
  90. gccpath = ""
  91. for p in os.environ["PATH"].split(";"):
  92. if os.path.exists(os.path.join(p, "gcc.exe")):
  93. gccpath = p
  94. break
  95. windowsIncludeDirs.append(os.path.abspath(os.path.join(gccpath, "..")))
  96. windowsIncludeDirs.append(os.path.abspath(os.path.join(gccpath, "..", "..")))
  97. possible_openssl_prefixes = None
  98. if sys.platform == 'win32':
  99. possible_openssl_prefixes = windowsIncludeDirs
  100. else:
  101. possible_openssl_prefixes = ['/usr', '/usr/local', '/usr/local/opt/openssl', '/usr/local/ssl', '/usr/local/openssl',
  102. '/usr/local/borg', '/opt/local', '/opt/pkg', ]
  103. if os.environ.get('BORG_OPENSSL_PREFIX'):
  104. possible_openssl_prefixes.insert(0, os.environ.get('BORG_OPENSSL_PREFIX'))
  105. ssl_prefix = detect_openssl(possible_openssl_prefixes)
  106. if not ssl_prefix:
  107. raise Exception('Unable to find OpenSSL >= 1.0 headers. (Looked here: {})'.format(', '.join(possible_openssl_prefixes)))
  108. include_dirs.append(os.path.join(ssl_prefix, 'include'))
  109. library_dirs.append(os.path.join(ssl_prefix, 'lib'))
  110. possible_lz4_prefixes = None
  111. if sys.platform == 'win32':
  112. possible_lz4_prefixes = windowsIncludeDirs
  113. else:
  114. possible_lz4_prefixes = ['/usr', '/usr/local', '/usr/local/opt/lz4', '/usr/local/lz4',
  115. '/usr/local/borg', '/opt/local', '/opt/pkg', ]
  116. if os.environ.get('BORG_LZ4_PREFIX'):
  117. possible_lz4_prefixes.insert(0, os.environ.get('BORG_LZ4_PREFIX'))
  118. lz4_prefix = detect_lz4(possible_lz4_prefixes)
  119. if lz4_prefix:
  120. include_dirs.append(os.path.join(lz4_prefix, 'include'))
  121. library_dirs.append(os.path.join(lz4_prefix, 'lib'))
  122. elif not on_rtd:
  123. raise Exception('Unable to find LZ4 headers. (Looked here: {})'.format(', '.join(possible_lz4_prefixes)))
  124. with open('README.rst', 'r') as fd:
  125. long_description = fd.read()
  126. class build_usage(Command):
  127. description = "generate usage for each command"
  128. user_options = [
  129. ('output=', 'O', 'output directory'),
  130. ]
  131. def initialize_options(self):
  132. pass
  133. def finalize_options(self):
  134. pass
  135. def run(self):
  136. print('generating usage docs')
  137. # allows us to build docs without the C modules fully loaded during help generation
  138. from borg.archiver import Archiver
  139. parser = Archiver().build_parser(prog='borg')
  140. choices = {}
  141. for action in parser._actions:
  142. if action.choices is not None:
  143. choices.update(action.choices)
  144. print('found commands: %s' % list(choices.keys()))
  145. if not os.path.exists('docs/usage'):
  146. os.mkdir('docs/usage')
  147. for command, parser in choices.items():
  148. print('generating help for %s' % command)
  149. with open('docs/usage/%s.rst.inc' % command, 'w') as doc:
  150. if command == 'help':
  151. for topic in Archiver.helptext:
  152. params = {"topic": topic,
  153. "underline": '~' * len('borg help ' + topic)}
  154. doc.write(".. _borg_{topic}:\n\n".format(**params))
  155. doc.write("borg help {topic}\n{underline}\n::\n\n".format(**params))
  156. doc.write(Archiver.helptext[topic])
  157. else:
  158. params = {"command": command,
  159. "underline": '-' * len('borg ' + command)}
  160. doc.write(".. _borg_{command}:\n\n".format(**params))
  161. doc.write("borg {command}\n{underline}\n::\n\n borg {command}".format(**params))
  162. self.write_usage(parser, doc)
  163. epilog = parser.epilog
  164. parser.epilog = None
  165. self.write_options(parser, doc)
  166. doc.write("\n\nDescription\n~~~~~~~~~~~\n")
  167. doc.write(epilog)
  168. common_options = [group for group in choices['create']._action_groups if group.title == 'Common options'][0]
  169. with open('docs/usage/common-options.rst.inc', 'w') as doc:
  170. self.write_options_group(common_options, doc, False)
  171. def write_usage(self, parser, fp):
  172. if any(len(o.option_strings) for o in parser._actions):
  173. fp.write(' <options>')
  174. for option in parser._actions:
  175. if option.option_strings:
  176. continue
  177. fp.write(' ' + option.metavar)
  178. def write_options(self, parser, fp):
  179. for group in parser._action_groups:
  180. if group.title == 'Common options':
  181. fp.write('\n\n`Common options`_\n')
  182. fp.write(' |')
  183. else:
  184. self.write_options_group(group, fp)
  185. def write_options_group(self, group, fp, with_title=True):
  186. def is_positional_group(group):
  187. return any(not o.option_strings for o in group._group_actions)
  188. def get_help(option):
  189. text = textwrap.dedent((option.help or '') % option.__dict__)
  190. return '\n'.join('| ' + line for line in text.splitlines())
  191. def shipout(text):
  192. fp.write(textwrap.indent('\n'.join(text), ' ' * 4))
  193. if not group._group_actions:
  194. return
  195. if with_title:
  196. fp.write('\n\n')
  197. fp.write(group.title + '\n')
  198. text = []
  199. if is_positional_group(group):
  200. for option in group._group_actions:
  201. text.append(option.metavar)
  202. text.append(textwrap.indent(option.help or '', ' ' * 4))
  203. shipout(text)
  204. return
  205. options = []
  206. for option in group._group_actions:
  207. if option.metavar:
  208. option_fmt = '``%%s %s``' % option.metavar
  209. else:
  210. option_fmt = '``%s``'
  211. option_str = ', '.join(option_fmt % s for s in option.option_strings)
  212. options.append((option_str, option))
  213. for option_str, option in options:
  214. help = textwrap.indent(get_help(option), ' ' * 4)
  215. text.append(option_str)
  216. text.append(help)
  217. shipout(text)
  218. class build_api(Command):
  219. description = "generate a basic api.rst file based on the modules available"
  220. user_options = [
  221. ('output=', 'O', 'output directory'),
  222. ]
  223. def initialize_options(self):
  224. pass
  225. def finalize_options(self):
  226. pass
  227. def run(self):
  228. print("auto-generating API documentation")
  229. with open("docs/api.rst", "w") as doc:
  230. doc.write("""
  231. API Documentation
  232. =================
  233. """)
  234. for mod in glob('borg/*.py') + glob('borg/*.pyx'):
  235. print("examining module %s" % mod)
  236. mod = mod.replace('.pyx', '').replace('.py', '').replace('/', '.')
  237. if "._" not in mod:
  238. doc.write("""
  239. .. automodule:: %s
  240. :members:
  241. :undoc-members:
  242. """ % mod)
  243. cmdclass = {
  244. 'build_ext': build_ext,
  245. 'build_api': build_api,
  246. 'build_usage': build_usage,
  247. 'sdist': Sdist
  248. }
  249. ext_modules = []
  250. if not on_rtd:
  251. ext_modules += [
  252. Extension('borg.compress', [compress_source], libraries=['lz4'], include_dirs=include_dirs, library_dirs=library_dirs),
  253. Extension('borg.crypto', [crypto_source], libraries=['crypto'], include_dirs=include_dirs, library_dirs=library_dirs),
  254. Extension('borg.chunker', [chunker_source]),
  255. Extension('borg.hashindex', [hashindex_source])
  256. ]
  257. if sys.platform == 'linux':
  258. ext_modules.append(Extension('borg.platform_linux', [platform_linux_source], libraries=['acl']))
  259. elif sys.platform.startswith('freebsd'):
  260. ext_modules.append(Extension('borg.platform_freebsd', [platform_freebsd_source]))
  261. elif sys.platform == 'darwin':
  262. ext_modules.append(Extension('borg.platform_darwin', [platform_darwin_source]))
  263. def parse(root, describe_command=None):
  264. file = open('borg/_version.py', 'w')
  265. output = subprocess.check_output("git describe --tags --long").decode().strip()
  266. file.write('version = "' + output + '"\n')
  267. return output
  268. parse_function = parse if sys.platform == 'win32' else None
  269. setup(
  270. name='borgbackup',
  271. use_scm_version={
  272. 'write_to': 'borg/_version.py',
  273. 'parse': parse_function,
  274. },
  275. author='The Borg Collective (see AUTHORS file)',
  276. author_email='borgbackup@python.org',
  277. url='https://borgbackup.readthedocs.io/',
  278. description='Deduplicated, encrypted, authenticated and compressed backups',
  279. long_description=long_description,
  280. license='BSD',
  281. platforms=['Linux', 'MacOS X', 'FreeBSD', 'OpenBSD', 'NetBSD', ],
  282. classifiers=[
  283. 'Development Status :: 4 - Beta',
  284. 'Environment :: Console',
  285. 'Intended Audience :: System Administrators',
  286. 'License :: OSI Approved :: BSD License',
  287. 'Operating System :: POSIX :: BSD :: FreeBSD',
  288. 'Operating System :: POSIX :: BSD :: OpenBSD',
  289. 'Operating System :: POSIX :: BSD :: NetBSD',
  290. 'Operating System :: MacOS :: MacOS X',
  291. 'Operating System :: POSIX :: Linux',
  292. 'Programming Language :: Python',
  293. 'Programming Language :: Python :: 3',
  294. 'Programming Language :: Python :: 3.4',
  295. 'Programming Language :: Python :: 3.5',
  296. 'Topic :: Security :: Cryptography',
  297. 'Topic :: System :: Archiving :: Backup',
  298. ],
  299. packages=['borg', 'borg.testsuite', ],
  300. entry_points={
  301. 'console_scripts': [
  302. 'borg = borg.archiver:main',
  303. 'borgfs = borg.archiver:main',
  304. ]
  305. },
  306. cmdclass=cmdclass,
  307. ext_modules=ext_modules,
  308. setup_requires=['setuptools_scm>=1.7'],
  309. install_requires=install_requires,
  310. extras_require=extras_require,
  311. )