setup.py 13 KB

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