setup.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  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 datetime
  26. import json
  27. import os
  28. import os.path
  29. import traceback
  30. import shutil
  31. from distutils import log
  32. from distutils.command.build import build as BuildCommand
  33. from distutils.core import Command
  34. from setuptools.command.sdist import sdist as SDistCommand
  35. from setuptools.command.develop import develop as DevelopCommand
  36. from setuptools import setup, find_packages
  37. from subprocess import check_output
  38. # The version of sentry
  39. VERSION = '8.0.0rc1'
  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. 'flake8>=2.0,<2.1',
  56. 'Babel',
  57. ]
  58. tests_require = [
  59. 'blist', # used by cassandra
  60. 'casscache',
  61. 'cqlsh',
  62. 'datadog',
  63. 'httpretty',
  64. 'pytest-cov>=1.8.0,<1.9.0',
  65. 'pytest-timeout>=1.0.0,<1.1.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. 'gunicorn>=19.2.1,<20.0.0',
  91. 'hiredis>=0.1.0,<0.2.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.5.0,<2.6.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.7.0,<2.8.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. 'urllib3>=1.11,<1.12',
  114. 'rb>=1.3.0,<2.0.0',
  115. ]
  116. postgres_requires = [
  117. ]
  118. postgres_pypy_requires = [
  119. 'psycopg2cffi',
  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 1
  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. # To find the default value of the inplace flag we inspect the
  168. # install and build_ext commands.
  169. install = self.distribution.get_command_obj('install')
  170. sdist = self.distribution.get_command_obj('sdist')
  171. build_ext = self.get_finalized_command('build_ext')
  172. # If we are not decided on in-place we are inplace if either
  173. # build_ext is inplace or we are invoked through the install
  174. # command (easiest check is to see if it's finalized).
  175. if self.inplace is None:
  176. self.inplace = (build_ext.inplace or install.finalized
  177. or sdist.finalized) and 1 or 0
  178. log.info('building JavaScript support.')
  179. # If we're coming from sdist, clear the hell out of the dist
  180. # folder first.
  181. if sdist.finalized:
  182. log.info('cleaning out dist folder')
  183. try:
  184. os.unlink('src/sentry/sentry-package.json')
  185. except OSError:
  186. pass
  187. try:
  188. shutil.rmtree('src/sentry/static/sentry/dist')
  189. except (OSError, IOError):
  190. pass
  191. log.info('cleaning out integration docs folder')
  192. try:
  193. shutil.rmtree(INTEGRATION_DOC_FOLDER)
  194. except (OSError, IOError):
  195. pass
  196. # In place means build_lib is src. We also log this.
  197. if self.inplace:
  198. log.info('In-place js building enabled')
  199. self.build_lib = 'src'
  200. # Otherwise we fetch build_lib from the build command.
  201. else:
  202. self.set_undefined_options('build',
  203. ('build_lib', 'build_lib'))
  204. log.info('regular js build: build path is %s' %
  205. self.build_lib)
  206. if self.work_path is None:
  207. self.work_path = ROOT
  208. def _get_package_version(self):
  209. """
  210. Attempt to get the most correct current version of Sentry.
  211. """
  212. pkg_path = os.path.join(self.work_path, 'src')
  213. sys.path.insert(0, pkg_path)
  214. try:
  215. import sentry
  216. except Exception:
  217. version = None
  218. build = None
  219. else:
  220. log.info("pulled version information from 'sentry' module".format(
  221. sentry.__file__))
  222. version = VERSION
  223. build = sentry.__build__
  224. finally:
  225. sys.path.pop(0)
  226. if not (version and build):
  227. try:
  228. with open(self.sentry_package_json_path) as fp:
  229. data = json.loads(fp.read())
  230. except Exception:
  231. pass
  232. else:
  233. log.info("pulled version information from 'sentry-package.json'")
  234. version, build = data['version'], data['build']
  235. return {
  236. 'version': version,
  237. 'build': build,
  238. }
  239. def _needs_static(self, version_info):
  240. json_path = self.sentry_package_json_path
  241. if not os.path.exists(json_path):
  242. return True
  243. with open(json_path) as fp:
  244. data = json.load(fp)
  245. if data.get('version') != version_info.get('version'):
  246. return True
  247. if data.get('build') != version_info.get('build'):
  248. return True
  249. return False
  250. def _needs_integration_docs(self):
  251. return not os.path.isdir(INTEGRATION_DOC_FOLDER)
  252. def run(self):
  253. need_integration_docs = not os.path.isdir(INTEGRATION_DOC_FOLDER)
  254. version_info = self._get_package_version()
  255. if not (self.force or self._needs_static(version_info)):
  256. log.info("skipped asset build (version already built)")
  257. else:
  258. log.info("building assets for Sentry v{} (build {})".format(
  259. version_info['version'] or 'UNKNOWN',
  260. version_info['build'] or 'UNKNOWN',
  261. ))
  262. if not version_info['version'] or not version_info['build']:
  263. log.fatal('Could not determine sentry version or build')
  264. sys.exit(1)
  265. try:
  266. self._build_static()
  267. except Exception:
  268. traceback.print_exc()
  269. log.fatal("unable to build Sentry's static assets!\n"
  270. "Hint: You might be running an invalid version of NPM.")
  271. sys.exit(1)
  272. log.info("writing version manifest")
  273. manifest = self._write_version_file(version_info)
  274. log.info("recorded manifest\n{}".format(
  275. json.dumps(manifest, indent=2),
  276. ))
  277. need_integration_docs = True
  278. if not need_integration_docs:
  279. log.info('skipped integration docs (already downloaded)')
  280. else:
  281. log.info('downloading integration docs')
  282. from sentry.utils.integrationdocs import sync_docs
  283. sync_docs()
  284. self.update_manifests()
  285. def update_manifests(self):
  286. # if we were invoked from sdist, we need to inform sdist about
  287. # which files we just generated. Otherwise they will be missing
  288. # in the manifest. This adds the files for what webpack generates
  289. # plus our own sentry-package.json file.
  290. sdist = self.distribution.get_command_obj('sdist')
  291. if not sdist.finalized:
  292. return
  293. # The path down from here only works for sdist:
  294. # Use the underlying file list so that we skip the file-exists
  295. # check which we do not want here.
  296. files = sdist.filelist.files
  297. base = os.path.abspath('.')
  298. # We need to split off the local parts of the files relative to
  299. # the current folder. This will chop off the right path for the
  300. # manifest.
  301. for root in self.sentry_static_dist_path, INTEGRATION_DOC_FOLDER:
  302. for dirname, dirnames, filenames in os.walk(root):
  303. for filename in filenames:
  304. filename = os.path.join(root, filename)
  305. files.append(filename[len(base):].lstrip(os.path.sep))
  306. files.append('src/sentry/sentry-package.json')
  307. def _build_static(self):
  308. work_path = self.work_path
  309. if os.path.exists(os.path.join(work_path, '.git')):
  310. log.info("initializing git submodules")
  311. check_output(['git', 'submodule', 'init'], cwd=work_path)
  312. check_output(['git', 'submodule', 'update'], cwd=work_path)
  313. log.info("running [npm install --quiet]")
  314. check_output(['npm', 'install', '--quiet'], cwd=work_path)
  315. # By setting NODE_ENV=production, a few things happen
  316. # * React optimizes out certain code paths
  317. # * Webpack will add version strings to built/referenced assets
  318. os.environ['NODE_ENV'] = 'production'
  319. log.info("running [webpack]")
  320. env = dict(os.environ)
  321. env['SENTRY_STATIC_DIST_PATH'] = self.sentry_static_dist_path
  322. check_output(['node_modules/.bin/webpack', '-p', '--bail'],
  323. cwd=work_path, env=env)
  324. def _write_version_file(self, version_info):
  325. manifest = {
  326. 'createdAt': datetime.datetime.utcnow().isoformat() + 'Z',
  327. 'version': version_info['version'],
  328. 'build': version_info['build'],
  329. }
  330. with open(self.sentry_package_json_path, 'w') as fp:
  331. json.dump(manifest, fp)
  332. return manifest
  333. @property
  334. def sentry_static_dist_path(self):
  335. return os.path.abspath(os.path.join(
  336. self.build_lib, 'sentry/static/sentry/dist'))
  337. @property
  338. def sentry_package_json_path(self):
  339. return os.path.abspath(os.path.join(
  340. self.build_lib, 'sentry/sentry-package.json'))
  341. class SentrySDistCommand(SDistCommand):
  342. # If we are not a light build we want to also execute build_js as
  343. # part of our source build pipeline.
  344. if not IS_LIGHT_BUILD:
  345. sub_commands = SDistCommand.sub_commands + \
  346. [('build_js', None)]
  347. class SentryBuildCommand(BuildCommand):
  348. def run(self):
  349. BuildCommand.run(self)
  350. if not IS_LIGHT_BUILD:
  351. self.run_command('build_js')
  352. class SentryDevelopCommand(DevelopCommand):
  353. def run(self):
  354. DevelopCommand.run(self)
  355. if not IS_LIGHT_BUILD:
  356. self.run_command('build_js')
  357. cmdclass = {
  358. 'sdist': SentrySDistCommand,
  359. 'develop': SentryDevelopCommand,
  360. 'build': SentryBuildCommand,
  361. 'build_js': BuildJavascriptCommand,
  362. }
  363. setup(
  364. name='sentry',
  365. version=VERSION,
  366. author='Sentry',
  367. author_email='hello@getsentry.com',
  368. url='https://getsentry.com',
  369. description='A realtime logging and aggregation server.',
  370. long_description=open(os.path.join(ROOT, 'README.rst')).read(),
  371. package_dir={'': 'src'},
  372. packages=find_packages('src'),
  373. zip_safe=False,
  374. install_requires=install_requires,
  375. extras_require={
  376. 'tests': tests_require,
  377. 'dev': dev_requires,
  378. 'postgres': install_requires + postgres_requires,
  379. 'postgres_pypy': install_requires + postgres_pypy_requires,
  380. },
  381. cmdclass=cmdclass,
  382. license='BSD',
  383. include_package_data=True,
  384. entry_points={
  385. 'console_scripts': [
  386. 'sentry = sentry.runner:main',
  387. ],
  388. 'flake8.extension': [
  389. ],
  390. },
  391. classifiers=[
  392. 'Framework :: Django',
  393. 'Intended Audience :: Developers',
  394. 'Intended Audience :: System Administrators',
  395. 'Operating System :: POSIX :: Linux',
  396. 'Topic :: Software Development'
  397. ],
  398. )