Gruntfile.coffee 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. fs = require "fs"
  2. spawn = require("child_process").spawn
  3. exec = require("child_process").exec
  4. rimraf = require "rimraf"
  5. Path = require "path"
  6. semver = require "semver"
  7. knox = require "knox"
  8. crypto = require "crypto"
  9. async = require "async"
  10. settings = require("settings-sharelatex")
  11. SERVICES = [{
  12. name: "web"
  13. repo: "https://github.com/sharelatex/web-sharelatex.git"
  14. version: "v0.1.2"
  15. }, {
  16. name: "real-time"
  17. repo: "https://github.com/sharelatex/real-time-sharelatex.git"
  18. version: "v0.1.2"
  19. }, {
  20. name: "document-updater"
  21. repo: "https://github.com/sharelatex/document-updater-sharelatex.git"
  22. version: "v0.1.2"
  23. }, {
  24. name: "clsi"
  25. repo: "https://github.com/sharelatex/clsi-sharelatex.git"
  26. version: "v0.1.2"
  27. }, {
  28. name: "filestore"
  29. repo: "https://github.com/sharelatex/filestore-sharelatex.git"
  30. version: "v0.1.2"
  31. }, {
  32. name: "track-changes"
  33. repo: "https://github.com/sharelatex/track-changes-sharelatex.git"
  34. version: "v0.1.2"
  35. }, {
  36. name: "docstore"
  37. repo: "https://github.com/sharelatex/docstore-sharelatex.git"
  38. version: "v0.1.2"
  39. }, {
  40. name: "chat"
  41. repo: "https://github.com/sharelatex/chat-sharelatex.git"
  42. version: "v0.1.2"
  43. }, {
  44. name: "tags"
  45. repo: "https://github.com/sharelatex/tags-sharelatex.git"
  46. version: "v0.1.2"
  47. }, {
  48. name: "spelling"
  49. repo: "https://github.com/sharelatex/spelling-sharelatex.git"
  50. version: "v0.1.2"
  51. }]
  52. module.exports = (grunt) ->
  53. grunt.loadNpmTasks 'grunt-bunyan'
  54. grunt.loadNpmTasks 'grunt-execute'
  55. grunt.loadNpmTasks 'grunt-available-tasks'
  56. grunt.loadNpmTasks 'grunt-concurrent'
  57. grunt.loadNpmTasks "grunt-contrib-coffee"
  58. grunt.loadNpmTasks "grunt-shell"
  59. execute = {}
  60. for service in SERVICES
  61. execute[service.name] =
  62. src: "#{service.name}/app.js"
  63. grunt.initConfig
  64. execute: execute
  65. concurrent:
  66. all:
  67. tasks: ("run:#{service.name}" for service in SERVICES)
  68. options:
  69. limit: SERVICES.length
  70. logConcurrentOutput: true
  71. coffee:
  72. migrate:
  73. expand: true,
  74. flatten: false,
  75. cwd: './',
  76. src: ['./migrations/*.coffee'],
  77. dest: './',
  78. ext: '.js'
  79. options:
  80. bare:true
  81. shell:
  82. migrate:
  83. command: "./node_modules/east/bin/east migrate --adapter east-mongo --url #{settings?.mongo?.url}"
  84. availabletasks:
  85. tasks:
  86. options:
  87. filter: 'exclude',
  88. tasks: [
  89. 'concurrent'
  90. 'execute'
  91. 'bunyan'
  92. 'availabletasks'
  93. ]
  94. groups:
  95. "Run tasks": [
  96. "run"
  97. "run:all"
  98. "default"
  99. ].concat ("run:#{service.name}" for service in SERVICES)
  100. "Misc": [
  101. "help"
  102. ]
  103. "Install tasks": ("install:#{service.name}" for service in SERVICES).concat(["install:all", "install", "install:dirs", "install:config"])
  104. "Update tasks": ("update:#{service.name}" for service in SERVICES).concat(["update:all", "update"])
  105. "Config tasks": ["install:config"]
  106. "Checks": ["check", "check:redis", "check:latexmk", "check:s3", "check:make"]
  107. for service in SERVICES
  108. do (service) ->
  109. grunt.registerTask "install:#{service.name}", "Download and set up the #{service.name} service", () ->
  110. done = @async()
  111. Helpers.installService(service, done)
  112. grunt.registerTask "update:#{service.name}", "Checkout and update the #{service.name} service", () ->
  113. done = @async()
  114. Helpers.updateService(service, done)
  115. grunt.registerTask "run:#{service.name}", "Run the ShareLaTeX #{service.name} service", ["bunyan", "execute:#{service.name}"]
  116. grunt.registerTask "release:#{service.name}", "Create a new release version of #{service.name} (specify with --release option)", () ->
  117. done = @async()
  118. Helpers.createNewRelease(service, grunt.option("release"), done)
  119. grunt.registerTask 'install:config', "Copy the example config into the real config", () ->
  120. Helpers.installConfig @async()
  121. grunt.registerTask 'install:dirs', "Copy the example config into the real config", () ->
  122. Helpers.createDataDirs @async()
  123. grunt.registerTask 'install:all', "Download and set up all ShareLaTeX services",
  124. ["check:make"].concat(
  125. ("install:#{service.name}" for service in SERVICES)
  126. ).concat(["install:config", "install:dirs"])
  127. grunt.registerTask 'install', 'install:all'
  128. grunt.registerTask 'update:all', "Checkout and update all ShareLaTeX services",
  129. ["check:make"].concat(
  130. ("update:#{service.name}" for service in SERVICES)
  131. )
  132. grunt.registerTask 'update', 'update:all'
  133. grunt.registerTask 'run', "Run all of the sharelatex processes", ['concurrent:all']
  134. grunt.registerTask 'run:all', 'run'
  135. grunt.registerTask 'help', 'Display this help list', 'availabletasks'
  136. grunt.registerTask 'default', 'run'
  137. grunt.registerTask "check:redis", "Check that redis is installed and running", () ->
  138. Helpers.checkRedis @async()
  139. grunt.registerTask "check:latexmk", "Check that latexmk is installed", () ->
  140. Helpers.checkLatexmk @async()
  141. grunt.registerTask "check:s3", "Check that Amazon S3 credentials are configured", () ->
  142. Helpers.checkS3 @async()
  143. grunt.registerTask "check:fs", "Check that local filesystem options are configured", () ->
  144. Helpers.checkFS @async()
  145. grunt.registerTask "check:aspell", "Check that aspell is installed", () ->
  146. Helpers.checkAspell @async()
  147. grunt.registerTask "check:make", "Check that make is installed", () ->
  148. Helpers.checkMake @async()
  149. grunt.registerTask "check", "Check that you have the required dependencies installed", ["check:redis", "check:latexmk", "check:s3", "check:fs", "check:aspell"]
  150. grunt.registerTask "build:deb", "Build an installable .deb file from the current directory", () ->
  151. Helpers.buildDeb @async()
  152. grunt.registerTask "build:upstart_scripts", "Create upstart scripts for each service", () ->
  153. Helpers.buildUpstartScripts()
  154. #grunt.registerTask 'migrate', "compile migrations and run them", ['coffee:migrate', 'shell:migrate']
  155. Helpers =
  156. installService: (service, callback = (error) ->) ->
  157. Helpers.cloneGitRepo service, (error) ->
  158. return callback(error) if error?
  159. Helpers.installNpmModules service, (error) ->
  160. return callback(error) if error?
  161. Helpers.rebuildNpmModules service, (error) ->
  162. return callback(error) if error?
  163. Helpers.runGruntInstall service, (error) ->
  164. return callback(error) if error?
  165. callback()
  166. updateService: (service, callback = (error) ->) ->
  167. Helpers.updateGitRepo service, (error) ->
  168. return callback(error) if error?
  169. Helpers.installNpmModules service, (error) ->
  170. return callback(error) if error?
  171. Helpers.rebuildNpmModules service, (error) ->
  172. return callback(error) if error?
  173. Helpers.runGruntInstall service, (error) ->
  174. return callback(error) if error?
  175. callback()
  176. cloneGitRepo: (service, callback = (error) ->) ->
  177. repo_src = service.repo
  178. dir = service.name
  179. if !fs.existsSync(dir)
  180. proc = spawn "git", [
  181. "clone",
  182. "-b", service.version,
  183. repo_src,
  184. dir
  185. ], stdio: "inherit"
  186. proc.on "close", () ->
  187. callback()
  188. else
  189. console.log "#{dir} already installed, skipping."
  190. callback()
  191. updateGitRepo: (service, callback = (error) ->) ->
  192. dir = service.name
  193. proc = spawn "git", ["checkout", service.version], cwd: dir, stdio: "inherit"
  194. proc.on "close", () ->
  195. proc = spawn "git", ["pull"], cwd: dir, stdio: "inherit"
  196. proc.on "close", () ->
  197. callback()
  198. createNewRelease: (service, version, callback = (error) ->) ->
  199. dir = service.name
  200. proc = spawn "sed", [
  201. "-i", "",
  202. "s/\"version\".*$/\"version\": \"#{version}\",/g",
  203. "package.json"
  204. ], cwd: dir, stdio: "inherit"
  205. proc.on "close", () ->
  206. proc = spawn "git", ["commit", "-a", "-m", "Release version #{version}"], cwd: dir, stdio: "inherit"
  207. proc.on "close", () ->
  208. proc = spawn "git", ["tag", "v#{version}"], cwd: dir, stdio: "inherit"
  209. proc.on "close", () ->
  210. proc = spawn "git", ["push"], cwd: dir, stdio: "inherit"
  211. proc.on "close", () ->
  212. proc = spawn "git", ["push", "--tags"], cwd: dir, stdio: "inherit"
  213. proc.on "close", () ->
  214. callback()
  215. installNpmModules: (service, callback = (error) ->) ->
  216. dir = service.name
  217. proc = spawn "npm", ["install"], stdio: "inherit", cwd: dir
  218. proc.on "close", () ->
  219. callback()
  220. # work around for https://github.com/npm/npm/issues/5400
  221. # where binary modules are not built due to bug in npm
  222. rebuildNpmModules: (service, callback = (error) ->) ->
  223. dir = service.name
  224. proc = spawn "npm", ["rebuild"], stdio: "inherit", cwd: dir
  225. proc.on "close", () ->
  226. callback()
  227. createDataDirs: (callback = (error) ->) ->
  228. DIRS = [
  229. "tmp/dumpFolder"
  230. "tmp/uploads"
  231. "data/user_files"
  232. "data/compiles"
  233. "data/cache"
  234. ]
  235. jobs = []
  236. for dir in DIRS
  237. do (dir) ->
  238. jobs.push (callback) ->
  239. path = Path.join(__dirname, dir)
  240. grunt.log.writeln "Ensuring '#{path}' exists"
  241. exec "mkdir -p #{path}", callback
  242. async.series jobs, callback
  243. installConfig: (callback = (error) ->) ->
  244. src = "config/settings.development.coffee.example"
  245. dest = "config/settings.development.coffee"
  246. if !fs.existsSync(dest)
  247. grunt.log.writeln "Creating config at #{dest}"
  248. config = fs.readFileSync(src).toString()
  249. config = config.replace /CRYPTO_RANDOM/g, () ->
  250. crypto.randomBytes(64).toString("hex")
  251. fs.writeFileSync dest, config
  252. callback()
  253. else
  254. grunt.log.writeln "Config file already exists. Skipping."
  255. callback()
  256. runGruntInstall: (service, callback = (error) ->) ->
  257. dir = service.name
  258. proc = spawn "grunt", ["install"], stdio: "inherit", cwd: dir
  259. proc.on "close", () ->
  260. callback()
  261. checkRedis: (callback = (error) ->) ->
  262. grunt.log.write "Checking Redis is running... "
  263. exec "redis-cli info", (error, stdout, stderr) ->
  264. if error? and error.message.match("Could not connect")
  265. grunt.log.error "FAIL. Redis is not running"
  266. return callback(error)
  267. else if error?
  268. return callback(error)
  269. else
  270. m = stdout.match(/redis_version:(.*)/)
  271. if !m?
  272. grunt.log.error "FAIL."
  273. grunt.log.error "Unknown redis version"
  274. error = new Error("Unknown redis version")
  275. else
  276. version = m[1]
  277. if semver.gte(version, "2.6.12")
  278. grunt.log.writeln "OK."
  279. grunt.log.writeln "Running Redis version #{version}"
  280. else
  281. grunt.log.error "FAIL."
  282. grunt.log.error "Redis version is too old (#{version}). Must be 2.6.12 or greater."
  283. error = new Error("Redis version is too old (#{version}). Must be 2.6.12 or greater.")
  284. callback(error)
  285. checkLatexmk: (callback = (error) ->) ->
  286. grunt.log.write "Checking latexmk is installed... "
  287. exec "latexmk --version", (error, stdout, stderr) ->
  288. if error? and error.message.match("not found")
  289. grunt.log.error "FAIL."
  290. grunt.log.errorlns """
  291. Either latexmk is not installed or is not in your PATH.
  292. latexmk comes with TexLive 2013, and must be a version from 2013 or later.
  293. If you have already have TeXLive installed, then make sure it is
  294. included in your PATH (example for 64-bit linux):
  295. export PATH=$PATH:/usr/local/texlive/2014/bin/x86_64-linux/
  296. This is a not a fatal error, but compiling will not work without latexmk.
  297. """
  298. return callback(error)
  299. else if error?
  300. return callback(error)
  301. else
  302. m = stdout.match(/Version (.*)/)
  303. if !m?
  304. grunt.log.error "FAIL."
  305. grunt.log.error "Unknown latexmk version"
  306. error = new Error("Unknown latexmk version")
  307. else
  308. version = m[1]
  309. if semver.gte(version + ".0", "4.39.0")
  310. grunt.log.writeln "OK."
  311. grunt.log.writeln "Running latexmk version #{version}"
  312. else
  313. grunt.log.error "FAIL."
  314. grunt.log.errorlns """
  315. latexmk version is too old (#{version}). Must be 4.39 or greater.
  316. This is a not a fatal error, but compiling will not work without latexmk
  317. """
  318. error = new Error("latexmk is too old")
  319. callback(error)
  320. checkAspell: (callback = (error) ->) ->
  321. grunt.log.write "Checking aspell is installed... "
  322. exec "aspell dump dicts", (error, stdout, stderr) ->
  323. if error? and error.message.match("not found")
  324. grunt.log.error "FAIL."
  325. grunt.log.errorlns """
  326. Either aspell is not installed or is not in your PATH.
  327. On Ubuntu you can install aspell with:
  328. sudo apt-get install aspell
  329. Or on a mac:
  330. brew install aspell
  331. This is not a fatal error, but the spell-checker will not work without aspell
  332. """
  333. return callback(error)
  334. else if error?
  335. return callback(error)
  336. else
  337. grunt.log.writeln "OK."
  338. grunt.log.writeln "The following spell check dictionaries are available:"
  339. grunt.log.write stdout
  340. callback()
  341. callback(error)
  342. checkS3: (callback = (error) ->) ->
  343. Settings = require "settings-sharelatex"
  344. if Settings.filestore.backend==""
  345. grunt.log.writeln "No backend specified. Assuming Amazon S3"
  346. Settings.filestore.backend = "s3"
  347. if Settings.filestore.backend=="s3"
  348. grunt.log.write "Checking S3 credentials... "
  349. try
  350. client = knox.createClient({
  351. key: Settings.filestore.s3.key
  352. secret: Settings.filestore.s3.secret
  353. bucket: Settings.filestore.stores.user_files
  354. })
  355. catch e
  356. grunt.log.error "FAIL."
  357. grunt.log.errorlns """
  358. Please configure your Amazon S3 credentials in config/settings.development.coffee
  359. Amazon S3 (Simple Storage Service) is a cloud storage service provided by
  360. Amazon. ShareLaTeX uses S3 for storing binary files like images. You can
  361. sign up for an account and find out more at:
  362. http://aws.amazon.com/s3/
  363. """
  364. return callback()
  365. client.getFile "does-not-exist", (error, response) ->
  366. unless response? and response.statusCode == 404
  367. grunt.log.error "FAIL."
  368. grunt.log.errorlns """
  369. Could not connect to Amazon S3. Please check your credentials.
  370. """
  371. else
  372. grunt.log.writeln "OK."
  373. callback()
  374. else
  375. grunt.log.writeln "Filestore other than S3 configured. Not checking S3."
  376. callback()
  377. checkFS: (callback = (error) ->) ->
  378. Settings = require "settings-sharelatex"
  379. if Settings.filestore.backend=="fs"
  380. grunt.log.write "Checking FS configuration... "
  381. fs = require("fs")
  382. fs.exists Settings.filestore.stores.user_files, (exists) ->
  383. if exists
  384. grunt.log.writeln "OK."
  385. else
  386. grunt.log.error "FAIL."
  387. grunt.log.errorlns """
  388. Could not find directory "#{Settings.filestore.stores.user_files}".
  389. Please check your configuration.
  390. """
  391. callback()
  392. else
  393. grunt.log.writeln "Filestore other than FS configured. Not checking FS."
  394. callback()
  395. checkMake: (callback = (error) ->) ->
  396. grunt.log.write "Checking make is installed... "
  397. exec "make --version", (error, stdout, stderr) ->
  398. if error? and error.message.match("not found")
  399. grunt.log.error "FAIL."
  400. grunt.log.errorlns """
  401. Either make is not installed or is not in your path.
  402. On Ubuntu you can install make with:
  403. sudo apt-get install build-essential
  404. """
  405. return callback(error)
  406. else if error?
  407. return callback(error)
  408. else
  409. grunt.log.write "OK."
  410. return callback()
  411. buildUpstartScripts: () ->
  412. template = fs.readFileSync("package/upstart/sharelatex-template").toString()
  413. for service in SERVICES
  414. fs.writeFileSync "package/upstart/sharelatex-#{service.name}", template.replace(/__SERVICE__/g, service.name)
  415. buildPackageSettingsFile: () ->
  416. config = fs.readFileSync("config/settings.development.coffee.example").toString()
  417. config = config.replace /DATA_DIR.*/, "DATA_DIR = '/var/lib/sharelatex/data'"
  418. config = config.replace /TMP_DIR.*/, "TMP_DIR = '/var/lib/sharelatex/tmp'"
  419. fs.writeFileSync "package/config/settings.coffee", config
  420. buildDeb: (callback = (error) ->) ->
  421. command = ["-s", "dir", "-t", "deb", "-n", "sharelatex", "-v", "0.0.1", "--verbose"]
  422. command.push(
  423. "--maintainer", "ShareLaTeX <team@sharelatex.com>"
  424. "--config-files", "/etc/sharelatex/settings.coffee"
  425. "--config-files", "/etc/nginx/conf.d/sharelatex.conf"
  426. "--directories", "/var/lib/sharelatex"
  427. "--directories", "/var/log/sharelatex"
  428. )
  429. command.push(
  430. "--depends", "redis-server > 2.6.12"
  431. "--depends", "mongodb-org > 2.4.0"
  432. "--depends", "nodejs > 0.10.0"
  433. )
  434. @buildPackageSettingsFile()
  435. @buildUpstartScripts()
  436. for service in SERVICES
  437. command.push(
  438. "--deb-upstart", "package/upstart/sharelatex-#{service.name}"
  439. )
  440. after_install_script = """
  441. #!/bin/sh
  442. # Create random secret keys (twice, once for http auth pass, once for cookie secret).
  443. sed -i "0,/CRYPTO_RANDOM/s/CRYPTO_RANDOM/$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 64 | head -n 1)/" /etc/sharelatex/settings.coffee
  444. sed -i "0,/CRYPTO_RANDOM/s/CRYPTO_RANDOM/$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 64 | head -n 1)/" /etc/sharelatex/settings.coffee
  445. sudo adduser --system --group --home /var/www/sharelatex --no-create-home sharelatex
  446. mkdir -p /var/log/sharelatex
  447. chown sharelatex:sharelatex /var/log/sharelatex
  448. mkdir -p /var/lib/sharelatex
  449. """
  450. for dir in ["data/user_files", "tmp/uploads", "data/compiles", "data/cache", "tmp/dumpFolder"]
  451. after_install_script += """
  452. mkdir -p /var/lib/sharelatex/#{dir}
  453. """
  454. after_install_script += """
  455. chown -R sharelatex:sharelatex /var/lib/sharelatex
  456. """
  457. for service in SERVICES
  458. after_install_script += "service sharelatex-#{service.name} restart\n"
  459. fs.writeFileSync "package/scripts/after_install.sh", after_install_script
  460. command.push("--after-install", "package/scripts/after_install.sh")
  461. command.push("--exclude", "**/.git")
  462. command.push("--exclude", "**/node_modules/grunt-*")
  463. for path in ["filestore/user_files", "filestore/uploads", "clsi/cache", "clsi/compiles"]
  464. command.push "--exclude", path
  465. for service in SERVICES
  466. command.push "#{service.name}=/var/www/sharelatex/"
  467. command.push(
  468. "package/config/settings.coffee=/etc/sharelatex/settings.coffee"
  469. "package/nginx/sharelatex=/etc/nginx/conf.d/sharelatex.conf"
  470. )
  471. console.log "fpm " + command.join(" ")
  472. proc = spawn "fpm", command, stdio: "inherit"
  473. proc.on "close", (code) ->
  474. if code != 0
  475. callback(new Error("exit code: #{code}"))
  476. else
  477. callback()