setup.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. #!/usr/bin/env python
  2. """
  3. Sentry
  4. ======
  5. Sentry is a realtime event logging and aggregation platform. It specializes
  6. in monitoring errors and extracting all the information needed to do a proper
  7. post-mortem without any of the hassle of the standard user feedback loop.
  8. Sentry is a Server
  9. ------------------
  10. The Sentry package, at its core, is just a simple server and web UI. It will
  11. handle authentication clients (such as `Raven
  12. <https://github.com/getsentry/raven-python>`_)
  13. and all of the logic behind storage and aggregation.
  14. That said, Sentry is not limited to Python. The primary implementation is in
  15. Python, but it contains a full API for sending events from any language, in
  16. any application.
  17. :copyright: (c) 2011-2014 by the Sentry Team, see AUTHORS for more details.
  18. :license: BSD, see LICENSE for more details.
  19. """
  20. from __future__ import absolute_import
  21. import sys
  22. if sys.version_info[:2] < (2, 7):
  23. print 'Error: Sentry requires Python 2.7'
  24. sys.exit(1)
  25. import os
  26. import json
  27. import shutil
  28. import os.path
  29. import datetime
  30. import traceback
  31. from distutils import log
  32. from subprocess import check_output
  33. from distutils.core import Command
  34. from distutils.command.build import build as BuildCommand
  35. from setuptools import setup, find_packages
  36. from setuptools.command.sdist import sdist as SDistCommand
  37. from setuptools.command.develop import develop as DevelopCommand
  38. # The version of sentry
  39. VERSION = '8.1.0'
  40. # Also see sentry.utils.integrationdocs.DOC_FOLDER
  41. INTEGRATION_DOC_FOLDER = os.path.join(os.path.abspath(
  42. os.path.dirname(__file__)), 'src', 'sentry', 'integration-docs')
  43. # Hack to prevent stupid "TypeError: 'NoneType' object is not callable" error
  44. # in multiprocessing/util.py _exit_function when running `python
  45. # setup.py test` (see
  46. # http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html)
  47. for m in ('multiprocessing', 'billiard'):
  48. try:
  49. __import__(m)
  50. except ImportError:
  51. pass
  52. ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__)))
  53. IS_LIGHT_BUILD = os.environ.get('SENTRY_LIGHT_BUILD') == '1'
  54. dev_requires = [
  55. 'Babel',
  56. 'flake8>=2.0,<2.1',
  57. 'isort>=4.2.2,<4.3.0',
  58. ]
  59. tests_require = [
  60. 'blist', # used by cassandra
  61. 'casscache',
  62. 'cqlsh',
  63. 'datadog',
  64. 'pytest-cov>=1.8.0,<1.9.0',
  65. 'pytest-timeout>=0.5.0,<0.6.0',
  66. 'pytest-xdist>=1.11.0,<1.12.0',
  67. 'python-coveralls',
  68. 'responses',
  69. ]
  70. install_requires = [
  71. 'BeautifulSoup>=3.2.1,<3.3.0',
  72. 'celery>=3.1.8,<3.1.19',
  73. 'click>=5.0,<7.0',
  74. 'cssutils>=0.9.9,<0.10.0',
  75. 'Django>=1.6.0,<1.7',
  76. 'django-bitfield>=1.7.0,<1.8.0',
  77. 'django-crispy-forms>=1.4.0,<1.5.0',
  78. 'django-debug-toolbar>=1.3.2,<1.4.0',
  79. 'django-paging>=0.2.5,<0.3.0',
  80. 'django-jsonfield>=0.9.13,<0.9.14',
  81. 'django-picklefield>=0.3.0,<0.4.0',
  82. 'django-recaptcha>=1.0.4,<1.1.0',
  83. 'django-social-auth>=0.7.28,<0.8.0',
  84. 'django-sudo>=1.2.0,<1.3.0',
  85. 'django-templatetag-sugar>=0.1.0',
  86. 'djangorestframework>=2.3.8,<2.4.0',
  87. 'email-reply-parser>=0.2.0,<0.3.0',
  88. 'enum34>=0.9.18,<0.10.0',
  89. 'exam>=0.5.1',
  90. 'hiredis>=0.1.0,<0.2.0',
  91. 'ipaddr>=2.1.11,<2.2.0',
  92. 'kombu==3.0.30',
  93. 'lxml>=3.4.1',
  94. 'mock>=0.8.0,<1.1',
  95. 'petname>=1.7,<1.8',
  96. 'progressbar>=2.2,<2.4',
  97. 'psycopg2>=2.5.0,<2.6.0',
  98. 'pytest>=2.6.4,<2.7.0',
  99. 'pytest-django>=2.9.1,<2.10.0',
  100. 'python-dateutil>=2.0.0,<3.0.0',
  101. 'python-memcached>=1.53,<2.0.0',
  102. 'PyYAML>=3.11,<4.0',
  103. 'raven>=5.3.0',
  104. 'redis>=2.10.3,<2.11.0',
  105. 'requests%s>=2.9.1,<2.10.0' % (not IS_LIGHT_BUILD and '[security]' or ''),
  106. 'simplejson>=3.2.0,<3.9.0',
  107. 'six>=1.6.0,<2.0.0',
  108. 'setproctitle>=1.1.7,<1.2.0',
  109. 'statsd>=3.1.0,<3.2.0',
  110. 'South==1.0.1',
  111. 'toronado>=0.0.4,<0.1.0',
  112. 'ua-parser>=0.6.1,<0.7.0',
  113. 'urllib3>=1.14,<1.15',
  114. 'uwsgi>2.0.0,<2.1.0',
  115. 'rb>=1.3.0,<2.0.0',
  116. ]
  117. postgres_requires = [
  118. ]
  119. postgres_pypy_requires = [
  120. 'psycopg2cffi',
  121. ]
  122. class BuildJavascriptCommand(Command):
  123. description = 'build javascript support files'
  124. user_options = [
  125. ('work-path=', 'w',
  126. "The working directory for source files. Defaults to ."),
  127. ('build-lib=', 'b',
  128. "directory for script runtime modules"),
  129. ('inplace', 'i',
  130. "ignore build-lib and put compiled javascript files into the source " +
  131. "directory alongside your pure Python modules"),
  132. ('force', 'f',
  133. "Force rebuilding of static content. Defaults to rebuilding on version "
  134. "change detection."),
  135. ]
  136. boolean_options = ['force']
  137. def initialize_options(self):
  138. self.build_lib = None
  139. self.force = None
  140. self.work_path = None
  141. self.inplace = None
  142. def finalize_options(self):
  143. # This requires some explanation. Basically what we want to do
  144. # here is to control if we want to build in-place or into the
  145. # build-lib folder. Traditionally this is set by the `inplace`
  146. # command line flag for build_ext. However as we are a subcommand
  147. # we need to grab this information from elsewhere.
  148. #
  149. # An in-place build puts the files generated into the source
  150. # folder, a regular build puts the files into the build-lib
  151. # folder.
  152. #
  153. # The following situations we need to cover:
  154. #
  155. # command default in-place
  156. # setup.py build_js 0
  157. # setup.py build_ext value of in-place for build_ext
  158. # setup.py build_ext --inplace 1
  159. # pip install --editable . 1
  160. # setup.py install 0
  161. # setup.py sdist 0
  162. # setup.py bdist_wheel 0
  163. #
  164. # The way this is achieved is that build_js is invoked by two
  165. # subcommands: bdist_ext (which is in our case always executed
  166. # due to a custom distribution) or sdist.
  167. #
  168. # Note: at one point install was an in-place build but it's not
  169. # quite sure why. In case a version of install breaks again:
  170. # installations via pip from git URLs definitely require the
  171. # in-place flag to be disabled. So we might need to detect
  172. # that separately.
  173. #
  174. # To find the default value of the inplace flag we inspect the
  175. # sdist and build_ext commands.
  176. sdist = self.distribution.get_command_obj('sdist')
  177. build_ext = self.get_finalized_command('build_ext')
  178. # If we are not decided on in-place we are inplace if either
  179. # build_ext is inplace or we are invoked through the install
  180. # command (easiest check is to see if it's finalized).
  181. if self.inplace is None:
  182. self.inplace = (build_ext.inplace or sdist.finalized) and 1 or 0
  183. log.info('building JavaScript support.')
  184. # If we're coming from sdist, clear the hell out of the dist
  185. # folder first.
  186. if sdist.finalized:
  187. log.info('cleaning out dist folder')
  188. try:
  189. os.unlink('src/sentry/sentry-package.json')
  190. except OSError:
  191. pass
  192. try:
  193. shutil.rmtree('src/sentry/static/sentry/dist')
  194. except (OSError, IOError):
  195. pass
  196. log.info('cleaning out integration docs folder')
  197. try:
  198. shutil.rmtree(INTEGRATION_DOC_FOLDER)
  199. except (OSError, IOError):
  200. pass
  201. # In place means build_lib is src. We also log this.
  202. if self.inplace:
  203. log.info('In-place js building enabled')
  204. self.build_lib = 'src'
  205. # Otherwise we fetch build_lib from the build command.
  206. else:
  207. self.set_undefined_options('build',
  208. ('build_lib', 'build_lib'))
  209. log.info('regular js build: build path is %s' %
  210. self.build_lib)
  211. if self.work_path is None:
  212. self.work_path = ROOT
  213. def _get_package_version(self):
  214. """
  215. Attempt to get the most correct current version of Sentry.
  216. """
  217. pkg_path = os.path.join(self.work_path, 'src')
  218. sys.path.insert(0, pkg_path)
  219. try:
  220. import sentry
  221. except Exception:
  222. version = None
  223. build = None
  224. else:
  225. log.info("pulled version information from 'sentry' module".format(
  226. sentry.__file__))
  227. version = VERSION
  228. build = sentry.__build__
  229. finally:
  230. sys.path.pop(0)
  231. if not (version and build):
  232. try:
  233. with open(self.sentry_package_json_path) as fp:
  234. data = json.loads(fp.read())
  235. except Exception:
  236. pass
  237. else:
  238. log.info("pulled version information from 'sentry-package.json'")
  239. version, build = data['version'], data['build']
  240. return {
  241. 'version': version,
  242. 'build': build,
  243. }
  244. def _needs_static(self, version_info):
  245. json_path = self.sentry_package_json_path
  246. if not os.path.exists(json_path):
  247. return True
  248. with open(json_path) as fp:
  249. data = json.load(fp)
  250. if data.get('version') != version_info.get('version'):
  251. return True
  252. if data.get('build') != version_info.get('build'):
  253. return True
  254. return False
  255. def _needs_integration_docs(self):
  256. return not os.path.isdir(INTEGRATION_DOC_FOLDER)
  257. def run(self):
  258. need_integration_docs = not os.path.isdir(INTEGRATION_DOC_FOLDER)
  259. version_info = self._get_package_version()
  260. if not (self.force or self._needs_static(version_info)):
  261. log.info("skipped asset build (version already built)")
  262. else:
  263. log.info("building assets for Sentry v{} (build {})".format(
  264. version_info['version'] or 'UNKNOWN',
  265. version_info['build'] or 'UNKNOWN',
  266. ))
  267. if not version_info['version'] or not version_info['build']:
  268. log.fatal('Could not determine sentry version or build')
  269. sys.exit(1)
  270. try:
  271. self._build_static()
  272. except Exception:
  273. traceback.print_exc()
  274. log.fatal("unable to build Sentry's static assets!\n"
  275. "Hint: You might be running an invalid version of NPM.")
  276. sys.exit(1)
  277. log.info("writing version manifest")
  278. manifest = self._write_version_file(version_info)
  279. log.info("recorded manifest\n{}".format(
  280. json.dumps(manifest, indent=2),
  281. ))
  282. need_integration_docs = True
  283. if not need_integration_docs:
  284. log.info('skipped integration docs (already downloaded)')
  285. else:
  286. log.info('downloading integration docs')
  287. from sentry.utils.integrationdocs import sync_docs
  288. sync_docs()
  289. self.update_manifests()
  290. def update_manifests(self):
  291. # if we were invoked from sdist, we need to inform sdist about
  292. # which files we just generated. Otherwise they will be missing
  293. # in the manifest. This adds the files for what webpack generates
  294. # plus our own sentry-package.json file.
  295. sdist = self.distribution.get_command_obj('sdist')
  296. if not sdist.finalized:
  297. return
  298. # The path down from here only works for sdist:
  299. # Use the underlying file list so that we skip the file-exists
  300. # check which we do not want here.
  301. files = sdist.filelist.files
  302. base = os.path.abspath('.')
  303. # We need to split off the local parts of the files relative to
  304. # the current folder. This will chop off the right path for the
  305. # manifest.
  306. for root in self.sentry_static_dist_path, INTEGRATION_DOC_FOLDER:
  307. for dirname, dirnames, filenames in os.walk(root):
  308. for filename in filenames:
  309. filename = os.path.join(root, filename)
  310. files.append(filename[len(base):].lstrip(os.path.sep))
  311. files.append('src/sentry/sentry-package.json')
  312. files.append('src/sentry/static/version')
  313. def _build_static(self):
  314. work_path = self.work_path
  315. if os.path.exists(os.path.join(work_path, '.git')):
  316. log.info("initializing git submodules")
  317. check_output(['git', 'submodule', 'init'], cwd=work_path)
  318. check_output(['git', 'submodule', 'update'], cwd=work_path)
  319. log.info("running [npm install --quiet]")
  320. check_output(['npm', 'install', '--quiet'], cwd=work_path)
  321. # By setting NODE_ENV=production, a few things happen
  322. # * React optimizes out certain code paths
  323. # * Webpack will add version strings to built/referenced assets
  324. os.environ['NODE_ENV'] = 'production'
  325. log.info("running [webpack]")
  326. env = dict(os.environ)
  327. env['SENTRY_STATIC_DIST_PATH'] = self.sentry_static_dist_path
  328. check_output(['node_modules/.bin/webpack', '-p', '--bail'],
  329. cwd=work_path, env=env)
  330. def _write_version_file(self, version_info):
  331. manifest = {
  332. 'createdAt': datetime.datetime.utcnow().isoformat() + 'Z',
  333. 'version': version_info['version'],
  334. 'build': version_info['build'],
  335. }
  336. with open(self.sentry_package_json_path, 'w') as fp:
  337. json.dump(manifest, fp)
  338. with open(self.sentry_static_version_path, 'w') as fp:
  339. fp.write(version_info['build'])
  340. return manifest
  341. @property
  342. def sentry_static_dist_path(self):
  343. return os.path.abspath(os.path.join(
  344. self.build_lib, 'sentry/static/sentry/dist'))
  345. @property
  346. def sentry_package_json_path(self):
  347. return os.path.abspath(os.path.join(
  348. self.build_lib, 'sentry/sentry-package.json'))
  349. @property
  350. def sentry_static_version_path(self):
  351. return os.path.abspath(os.path.join(
  352. self.build_lib, 'sentry/static/version'))
  353. class SentrySDistCommand(SDistCommand):
  354. # If we are not a light build we want to also execute build_js as
  355. # part of our source build pipeline.
  356. if not IS_LIGHT_BUILD:
  357. sub_commands = SDistCommand.sub_commands + \
  358. [('build_js', None)]
  359. class SentryBuildCommand(BuildCommand):
  360. def run(self):
  361. BuildCommand.run(self)
  362. if not IS_LIGHT_BUILD:
  363. self.run_command('build_js')
  364. class SentryDevelopCommand(DevelopCommand):
  365. def run(self):
  366. DevelopCommand.run(self)
  367. if not IS_LIGHT_BUILD:
  368. self.run_command('build_js')
  369. cmdclass = {
  370. 'sdist': SentrySDistCommand,
  371. 'develop': SentryDevelopCommand,
  372. 'build': SentryBuildCommand,
  373. 'build_js': BuildJavascriptCommand,
  374. }
  375. setup(
  376. name='sentry',
  377. version=VERSION,
  378. author='Sentry',
  379. author_email='hello@getsentry.com',
  380. url='https://getsentry.com',
  381. description='A realtime logging and aggregation server.',
  382. long_description=open(os.path.join(ROOT, 'README.rst')).read(),
  383. package_dir={'': 'src'},
  384. packages=find_packages('src'),
  385. zip_safe=False,
  386. install_requires=install_requires,
  387. extras_require={
  388. 'tests': tests_require,
  389. 'dev': dev_requires,
  390. 'postgres': install_requires + postgres_requires,
  391. 'postgres_pypy': install_requires + postgres_pypy_requires,
  392. },
  393. cmdclass=cmdclass,
  394. license='BSD',
  395. include_package_data=True,
  396. entry_points={
  397. 'console_scripts': [
  398. 'sentry = sentry.runner:main',
  399. ],
  400. 'flake8.extension': [
  401. ],
  402. },
  403. classifiers=[
  404. 'Framework :: Django',
  405. 'Intended Audience :: Developers',
  406. 'Intended Audience :: System Administrators',
  407. 'Operating System :: POSIX :: Linux',
  408. 'Topic :: Software Development'
  409. ],
  410. )