setup.py 15 KB

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