setup.py 14 KB

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