setup.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  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.4.0.dev0'
  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,<1.2.0',
  89. 'exam>=0.5.1',
  90. 'hiredis>=0.1.0,<0.2.0',
  91. 'honcho>=0.7.0,<0.8.0',
  92. 'ipaddr>=2.1.11,<2.2.0',
  93. 'kombu==3.0.30',
  94. 'lxml>=3.4.1',
  95. 'mock>=0.8.0,<1.1',
  96. 'petname>=1.7,<1.8',
  97. 'progressbar>=2.2,<2.4',
  98. 'psycopg2>=2.6.0,<2.7.0',
  99. 'pytest>=2.6.4,<2.7.0',
  100. 'pytest-django>=2.9.1,<2.10.0',
  101. 'python-dateutil>=2.0.0,<3.0.0',
  102. 'python-memcached>=1.53,<2.0.0',
  103. 'PyYAML>=3.11,<4.0',
  104. 'raven>=5.3.0',
  105. 'redis>=2.10.3,<2.11.0',
  106. 'requests%s>=2.9.1,<2.10.0' % (not IS_LIGHT_BUILD and '[security]' or ''),
  107. 'simplejson>=3.2.0,<3.9.0',
  108. 'six>=1.6.0,<2.0.0',
  109. 'setproctitle>=1.1.7,<1.2.0',
  110. 'statsd>=3.1.0,<3.2.0',
  111. 'South==1.0.1',
  112. 'toronado>=0.0.4,<0.1.0',
  113. 'ua-parser>=0.6.1,<0.8.0',
  114. 'urllib3>=1.14,<1.15',
  115. 'uwsgi>2.0.0,<2.1.0',
  116. 'rb>=1.4.0,<2.0.0',
  117. ]
  118. dsym_requires = [
  119. 'symsynd>=0.5.2,<1.0.0',
  120. ]
  121. class BuildJavascriptCommand(Command):
  122. description = 'build javascript support files'
  123. user_options = [
  124. ('work-path=', 'w',
  125. "The working directory for source files. Defaults to ."),
  126. ('build-lib=', 'b',
  127. "directory for script runtime modules"),
  128. ('inplace', 'i',
  129. "ignore build-lib and put compiled javascript files into the source " +
  130. "directory alongside your pure Python modules"),
  131. ('force', 'f',
  132. "Force rebuilding of static content. Defaults to rebuilding on version "
  133. "change detection."),
  134. ]
  135. boolean_options = ['force']
  136. def initialize_options(self):
  137. self.build_lib = None
  138. self.force = None
  139. self.work_path = None
  140. self.inplace = None
  141. def finalize_options(self):
  142. # This requires some explanation. Basically what we want to do
  143. # here is to control if we want to build in-place or into the
  144. # build-lib folder. Traditionally this is set by the `inplace`
  145. # command line flag for build_ext. However as we are a subcommand
  146. # we need to grab this information from elsewhere.
  147. #
  148. # An in-place build puts the files generated into the source
  149. # folder, a regular build puts the files into the build-lib
  150. # folder.
  151. #
  152. # The following situations we need to cover:
  153. #
  154. # command default in-place
  155. # setup.py build_js 0
  156. # setup.py build_ext value of in-place for build_ext
  157. # setup.py build_ext --inplace 1
  158. # pip install --editable . 1
  159. # setup.py install 0
  160. # setup.py sdist 0
  161. # setup.py bdist_wheel 0
  162. #
  163. # The way this is achieved is that build_js is invoked by two
  164. # subcommands: bdist_ext (which is in our case always executed
  165. # due to a custom distribution) or sdist.
  166. #
  167. # Note: at one point install was an in-place build but it's not
  168. # quite sure why. In case a version of install breaks again:
  169. # installations via pip from git URLs definitely require the
  170. # in-place flag to be disabled. So we might need to detect
  171. # that separately.
  172. #
  173. # To find the default value of the inplace flag we inspect the
  174. # sdist and build_ext commands.
  175. sdist = self.distribution.get_command_obj('sdist')
  176. build_ext = self.get_finalized_command('build_ext')
  177. # If we are not decided on in-place we are inplace if either
  178. # build_ext is inplace or we are invoked through the install
  179. # command (easiest check is to see if it's finalized).
  180. if self.inplace is None:
  181. self.inplace = (build_ext.inplace or sdist.finalized) and 1 or 0
  182. log.info('building JavaScript support.')
  183. # If we're coming from sdist, clear the hell out of the dist
  184. # folder first.
  185. if sdist.finalized:
  186. log.info('cleaning out dist folder')
  187. try:
  188. os.unlink('src/sentry/sentry-package.json')
  189. except OSError:
  190. pass
  191. try:
  192. shutil.rmtree('src/sentry/static/sentry/dist')
  193. except (OSError, IOError):
  194. pass
  195. log.info('cleaning out integration docs folder')
  196. try:
  197. shutil.rmtree(INTEGRATION_DOC_FOLDER)
  198. except (OSError, IOError):
  199. pass
  200. # In place means build_lib is src. We also log this.
  201. if self.inplace:
  202. log.info('In-place js building enabled')
  203. self.build_lib = 'src'
  204. # Otherwise we fetch build_lib from the build command.
  205. else:
  206. self.set_undefined_options('build',
  207. ('build_lib', 'build_lib'))
  208. log.info('regular js build: build path is %s' %
  209. self.build_lib)
  210. if self.work_path is None:
  211. self.work_path = ROOT
  212. def _get_package_version(self):
  213. """
  214. Attempt to get the most correct current version of Sentry.
  215. """
  216. pkg_path = os.path.join(self.work_path, 'src')
  217. sys.path.insert(0, pkg_path)
  218. try:
  219. import sentry
  220. except Exception:
  221. version = None
  222. build = None
  223. else:
  224. log.info("pulled version information from 'sentry' module".format(
  225. sentry.__file__))
  226. version = VERSION
  227. build = sentry.__build__
  228. finally:
  229. sys.path.pop(0)
  230. if not (version and build):
  231. try:
  232. with open(self.sentry_package_json_path) as fp:
  233. data = json.loads(fp.read())
  234. except Exception:
  235. pass
  236. else:
  237. log.info("pulled version information from 'sentry-package.json'")
  238. version, build = data['version'], data['build']
  239. return {
  240. 'version': version,
  241. 'build': build,
  242. }
  243. def _needs_static(self, version_info):
  244. json_path = self.sentry_package_json_path
  245. if not os.path.exists(json_path):
  246. return True
  247. with open(json_path) as fp:
  248. data = json.load(fp)
  249. if data.get('version') != version_info.get('version'):
  250. return True
  251. if data.get('build') != version_info.get('build'):
  252. return True
  253. return False
  254. def _needs_integration_docs(self):
  255. return not os.path.isdir(INTEGRATION_DOC_FOLDER)
  256. def run(self):
  257. need_integration_docs = not os.path.isdir(INTEGRATION_DOC_FOLDER)
  258. version_info = self._get_package_version()
  259. if not (self.force or self._needs_static(version_info)):
  260. log.info("skipped asset build (version already built)")
  261. else:
  262. log.info("building assets for Sentry v{} (build {})".format(
  263. version_info['version'] or 'UNKNOWN',
  264. version_info['build'] or 'UNKNOWN',
  265. ))
  266. if not version_info['version'] or not version_info['build']:
  267. log.fatal('Could not determine sentry version or build')
  268. sys.exit(1)
  269. node_version = []
  270. for app in 'node', 'npm':
  271. try:
  272. node_version.append(check_output([app, '--version']).rstrip())
  273. except OSError:
  274. log.fatal('Cannot find `{0}` executable. Please install {0}`'
  275. ' and try again.'.format(app))
  276. sys.exit(1)
  277. log.info('using node ({}) and npm ({})'.format(*node_version))
  278. try:
  279. self._build_static()
  280. except Exception:
  281. traceback.print_exc()
  282. log.fatal("unable to build Sentry's static assets!\n"
  283. "Hint: You might be running an invalid version of NPM.")
  284. sys.exit(1)
  285. log.info("writing version manifest")
  286. manifest = self._write_version_file(version_info)
  287. log.info("recorded manifest\n{}".format(
  288. json.dumps(manifest, indent=2),
  289. ))
  290. need_integration_docs = True
  291. if not need_integration_docs:
  292. log.info('skipped integration docs (already downloaded)')
  293. else:
  294. log.info('downloading integration docs')
  295. from sentry.utils.integrationdocs import sync_docs
  296. sync_docs()
  297. self.update_manifests()
  298. def update_manifests(self):
  299. # if we were invoked from sdist, we need to inform sdist about
  300. # which files we just generated. Otherwise they will be missing
  301. # in the manifest. This adds the files for what webpack generates
  302. # plus our own sentry-package.json file.
  303. sdist = self.distribution.get_command_obj('sdist')
  304. if not sdist.finalized:
  305. return
  306. # The path down from here only works for sdist:
  307. # Use the underlying file list so that we skip the file-exists
  308. # check which we do not want here.
  309. files = sdist.filelist.files
  310. base = os.path.abspath('.')
  311. # We need to split off the local parts of the files relative to
  312. # the current folder. This will chop off the right path for the
  313. # manifest.
  314. for root in self.sentry_static_dist_path, INTEGRATION_DOC_FOLDER:
  315. for dirname, _, filenames in os.walk(root):
  316. for filename in filenames:
  317. filename = os.path.join(dirname, filename)
  318. files.append(filename[len(base):].lstrip(os.path.sep))
  319. files.append('src/sentry/sentry-package.json')
  320. files.append('src/sentry/static/version')
  321. def _build_static(self):
  322. work_path = self.work_path
  323. if os.path.exists(os.path.join(work_path, '.git')):
  324. log.info("initializing git submodules")
  325. check_output(['git', 'submodule', 'init'], cwd=work_path)
  326. check_output(['git', 'submodule', 'update'], cwd=work_path)
  327. log.info("running [npm install --quiet]")
  328. check_output(['npm', 'install', '--quiet'], cwd=work_path)
  329. # By setting NODE_ENV=production, a few things happen
  330. # * React optimizes out certain code paths
  331. # * Webpack will add version strings to built/referenced assets
  332. log.info("running [webpack]")
  333. env = dict(os.environ)
  334. env['SENTRY_STATIC_DIST_PATH'] = self.sentry_static_dist_path
  335. env['NODE_ENV'] = 'production'
  336. check_output(['node_modules/.bin/webpack', '-p', '--bail'],
  337. cwd=work_path, env=env)
  338. def _write_version_file(self, version_info):
  339. manifest = {
  340. 'createdAt': datetime.datetime.utcnow().isoformat() + 'Z',
  341. 'version': version_info['version'],
  342. 'build': version_info['build'],
  343. }
  344. with open(self.sentry_package_json_path, 'w') as fp:
  345. json.dump(manifest, fp)
  346. with open(self.sentry_static_version_path, 'w') as fp:
  347. fp.write(version_info['build'])
  348. return manifest
  349. @property
  350. def sentry_static_dist_path(self):
  351. return os.path.abspath(os.path.join(
  352. self.build_lib, 'sentry/static/sentry/dist'))
  353. @property
  354. def sentry_package_json_path(self):
  355. return os.path.abspath(os.path.join(
  356. self.build_lib, 'sentry/sentry-package.json'))
  357. @property
  358. def sentry_static_version_path(self):
  359. return os.path.abspath(os.path.join(
  360. self.build_lib, 'sentry/static/version'))
  361. class SentrySDistCommand(SDistCommand):
  362. # If we are not a light build we want to also execute build_js as
  363. # part of our source build pipeline.
  364. if not IS_LIGHT_BUILD:
  365. sub_commands = SDistCommand.sub_commands + \
  366. [('build_js', None)]
  367. class SentryBuildCommand(BuildCommand):
  368. def run(self):
  369. BuildCommand.run(self)
  370. if not IS_LIGHT_BUILD:
  371. self.run_command('build_js')
  372. class SentryDevelopCommand(DevelopCommand):
  373. def run(self):
  374. DevelopCommand.run(self)
  375. if not IS_LIGHT_BUILD:
  376. self.run_command('build_js')
  377. cmdclass = {
  378. 'sdist': SentrySDistCommand,
  379. 'develop': SentryDevelopCommand,
  380. 'build': SentryBuildCommand,
  381. 'build_js': BuildJavascriptCommand,
  382. }
  383. setup(
  384. name='sentry',
  385. version=VERSION,
  386. author='Sentry',
  387. author_email='hello@getsentry.com',
  388. url='https://getsentry.com',
  389. description='A realtime logging and aggregation server.',
  390. long_description=open(os.path.join(ROOT, 'README.rst')).read(),
  391. package_dir={'': 'src'},
  392. packages=find_packages('src'),
  393. zip_safe=False,
  394. install_requires=install_requires,
  395. extras_require={
  396. 'tests': tests_require,
  397. 'dev': dev_requires,
  398. 'postgres': install_requires,
  399. 'dsym': dsym_requires,
  400. },
  401. cmdclass=cmdclass,
  402. license='BSD',
  403. include_package_data=True,
  404. entry_points={
  405. 'console_scripts': [
  406. 'sentry = sentry.runner:main',
  407. ],
  408. 'flake8.extension': [
  409. ],
  410. },
  411. classifiers=[
  412. 'Framework :: Django',
  413. 'Intended Audience :: Developers',
  414. 'Intended Audience :: System Administrators',
  415. 'Operating System :: POSIX :: Linux',
  416. 'Topic :: Software Development'
  417. ],
  418. )