install.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // Copyright 2012 The Obvious Corporation.
  2. /*
  3. * This simply fetches the right version of phantom for the current platform.
  4. */
  5. 'use strict'
  6. var requestProgress = require('request-progress')
  7. var progress = require('progress')
  8. var AdmZip = require('adm-zip')
  9. var cp = require('child_process')
  10. var fs = require('fs-extra')
  11. var helper = require('./lib/phantomjs')
  12. var kew = require('kew')
  13. var npmconf = require('npmconf')
  14. var path = require('path')
  15. var request = require('request')
  16. var url = require('url')
  17. var util = require('util')
  18. var which = require('which')
  19. var cdnUrl = process.env.npm_config_phantomjs_cdnurl || process.env.PHANTOMJS_CDNURL || 'https://bitbucket.org/ariya/phantomjs/downloads'
  20. var downloadUrl = cdnUrl + '/phantomjs-' + helper.version + '-'
  21. var originalPath = process.env.PATH
  22. // If the process exits without going through exit(), then we did not complete.
  23. var validExit = false
  24. process.on('exit', function () {
  25. if (!validExit) {
  26. console.log('Install exited unexpectedly')
  27. exit(1)
  28. }
  29. })
  30. // NPM adds bin directories to the path, which will cause `which` to find the
  31. // bin for this package not the actual phantomjs bin. Also help out people who
  32. // put ./bin on their path
  33. process.env.PATH = helper.cleanPath(originalPath)
  34. var libPath = path.join(__dirname, 'lib')
  35. var pkgPath = path.join(libPath, 'phantom')
  36. var phantomPath = null
  37. var tmpPath = null
  38. // If the user manually installed PhantomJS, we want
  39. // to use the existing version.
  40. //
  41. // Do not re-use a manually-installed PhantomJS with
  42. // a different version.
  43. //
  44. // Do not re-use an npm-installed PhantomJS, because
  45. // that can lead to weird circular dependencies between
  46. // local versions and global versions.
  47. // https://github.com/Obvious/phantomjs/issues/85
  48. // https://github.com/Medium/phantomjs/pull/184
  49. var whichDeferred = kew.defer()
  50. which('phantomjs', whichDeferred.makeNodeResolver())
  51. whichDeferred.promise
  52. .then(function (result) {
  53. phantomPath = result
  54. // Horrible hack to avoid problems during global install. We check to see if
  55. // the file `which` found is our own bin script.
  56. if (phantomPath.indexOf(path.join('npm', 'phantomjs')) !== -1) {
  57. console.log('Looks like an `npm install -g` on windows; unable to check for already installed version.')
  58. throw new Error('Global install')
  59. }
  60. var contents = fs.readFileSync(phantomPath, 'utf8')
  61. if (/NPM_INSTALL_MARKER/.test(contents)) {
  62. console.log('Looks like an `npm install -g`; unable to check for already installed version.')
  63. throw new Error('Global install')
  64. } else {
  65. var checkVersionDeferred = kew.defer()
  66. cp.execFile(phantomPath, ['--version'], checkVersionDeferred.makeNodeResolver())
  67. return checkVersionDeferred.promise
  68. }
  69. })
  70. .then(function (stdout) {
  71. var version = stdout.trim()
  72. if (helper.version == version) {
  73. writeLocationFile(phantomPath);
  74. console.log('PhantomJS is already installed at', phantomPath + '.')
  75. exit(0)
  76. } else {
  77. console.log('PhantomJS detected, but wrong version', stdout.trim(), '@', phantomPath + '.')
  78. throw new Error('Wrong version')
  79. }
  80. })
  81. .fail(function (err) {
  82. // Trying to use a local file failed, so initiate download and install
  83. // steps instead.
  84. var npmconfDeferred = kew.defer()
  85. npmconf.load(npmconfDeferred.makeNodeResolver())
  86. return npmconfDeferred.promise
  87. })
  88. .then(function (conf) {
  89. tmpPath = findSuitableTempDirectory(conf)
  90. // Can't use a global version so start a download.
  91. if (process.platform === 'linux' && process.arch === 'x64') {
  92. downloadUrl += 'linux-x86_64.tar.bz2'
  93. } else if (process.platform === 'linux') {
  94. downloadUrl += 'linux-i686.tar.bz2'
  95. } else if (process.platform === 'darwin' || process.platform === 'openbsd' || process.platform === 'freebsd') {
  96. downloadUrl += 'macosx.zip'
  97. } else if (process.platform === 'win32') {
  98. downloadUrl += 'windows.zip'
  99. } else {
  100. console.error('Unexpected platform or architecture:', process.platform, process.arch)
  101. exit(1)
  102. }
  103. var fileName = downloadUrl.split('/').pop()
  104. var downloadedFile = path.join(tmpPath, fileName)
  105. // Start the install.
  106. if (!fs.existsSync(downloadedFile)) {
  107. console.log('Downloading', downloadUrl)
  108. console.log('Saving to', downloadedFile)
  109. return requestBinary(getRequestOptions(conf), downloadedFile)
  110. } else {
  111. console.log('Download already available at', downloadedFile)
  112. return downloadedFile
  113. }
  114. })
  115. .then(function (downloadedFile) {
  116. return extractDownload(downloadedFile)
  117. })
  118. .then(function (extractedPath) {
  119. return copyIntoPlace(extractedPath, pkgPath)
  120. })
  121. .then(function () {
  122. var location = process.platform === 'win32' ?
  123. path.join(pkgPath, 'phantomjs.exe') :
  124. path.join(pkgPath, 'bin' ,'phantomjs')
  125. var relativeLocation = path.relative(libPath, location)
  126. writeLocationFile(relativeLocation)
  127. // Ensure executable is executable by all users
  128. fs.chmodSync(location, '755')
  129. console.log('Done. Phantomjs binary available at', location)
  130. exit(0)
  131. })
  132. .fail(function (err) {
  133. console.error('Phantom installation failed', err, err.stack)
  134. exit(1)
  135. })
  136. function writeLocationFile(location) {
  137. console.log('Writing location.js file')
  138. if (process.platform === 'win32') {
  139. location = location.replace(/\\/g, '\\\\')
  140. }
  141. fs.writeFileSync(path.join(libPath, 'location.js'),
  142. 'module.exports.location = "' + location + '"')
  143. }
  144. function exit(code) {
  145. validExit = true
  146. process.env.PATH = originalPath
  147. process.exit(code || 0)
  148. }
  149. function findSuitableTempDirectory(npmConf) {
  150. var now = Date.now()
  151. var candidateTmpDirs = [
  152. process.env.TMPDIR || process.env.TEMP || npmConf.get('tmp'),
  153. '/tmp',
  154. path.join(process.cwd(), 'tmp')
  155. ]
  156. for (var i = 0; i < candidateTmpDirs.length; i++) {
  157. var candidatePath = path.join(candidateTmpDirs[i], 'phantomjs')
  158. try {
  159. fs.mkdirsSync(candidatePath, '0777')
  160. // Make double sure we have 0777 permissions; some operating systems
  161. // default umask does not allow write by default.
  162. fs.chmodSync(candidatePath, '0777')
  163. var testFile = path.join(candidatePath, now + '.tmp')
  164. fs.writeFileSync(testFile, 'test')
  165. fs.unlinkSync(testFile)
  166. return candidatePath
  167. } catch (e) {
  168. console.log(candidatePath, 'is not writable:', e.message)
  169. }
  170. }
  171. console.error('Can not find a writable tmp directory, please report issue ' +
  172. 'on https://github.com/Obvious/phantomjs/issues/59 with as much ' +
  173. 'information as possible.')
  174. exit(1)
  175. }
  176. function getRequestOptions(conf) {
  177. var strictSSL = conf.get('strict-ssl')
  178. if (process.version == 'v0.10.34') {
  179. console.log('Node v0.10.34 detected, turning off strict ssl due to https://github.com/joyent/node/issues/8894')
  180. strictSSL = false
  181. }
  182. var options = {
  183. uri: downloadUrl,
  184. encoding: null, // Get response as a buffer
  185. followRedirect: true, // The default download path redirects to a CDN URL.
  186. headers: {},
  187. strictSSL: strictSSL
  188. }
  189. var proxyUrl = conf.get('https-proxy') || conf.get('http-proxy') || conf.get('proxy')
  190. if (proxyUrl) {
  191. // Print using proxy
  192. var proxy = url.parse(proxyUrl)
  193. if (proxy.auth) {
  194. // Mask password
  195. proxy.auth = proxy.auth.replace(/:.*$/, ':******')
  196. }
  197. console.log('Using proxy ' + url.format(proxy))
  198. // Enable proxy
  199. options.proxy = proxyUrl
  200. // If going through proxy, use the user-agent string from the npm config
  201. options.headers['User-Agent'] = conf.get('user-agent')
  202. }
  203. // Use certificate authority settings from npm
  204. var ca = conf.get('ca')
  205. if (ca) {
  206. console.log('Using npmconf ca')
  207. options.ca = ca
  208. }
  209. return options
  210. }
  211. function requestBinary(requestOptions, filePath) {
  212. var deferred = kew.defer()
  213. var count = 0
  214. var notifiedCount = 0
  215. var writePath = filePath + '-download-' + Date.now()
  216. console.log('Receiving...')
  217. var bar = null
  218. requestProgress(request(requestOptions, function (error, response, body) {
  219. console.log('');
  220. if (!error && response.statusCode === 200) {
  221. fs.writeFileSync(writePath, body)
  222. console.log('Received ' + Math.floor(body.length / 1024) + 'K total.')
  223. fs.renameSync(writePath, filePath)
  224. deferred.resolve(filePath)
  225. } else if (response) {
  226. console.error('Error requesting archive.\n' +
  227. 'Status: ' + response.statusCode + '\n' +
  228. 'Request options: ' + JSON.stringify(requestOptions, null, 2) + '\n' +
  229. 'Response headers: ' + JSON.stringify(response.headers, null, 2) + '\n' +
  230. 'Make sure your network and proxy settings are correct.\n\n' +
  231. 'If you continue to have issues, please report this full log at ' +
  232. 'https://github.com/Medium/phantomjs')
  233. exit(1)
  234. } else if (error && error.stack && error.stack.indexOf('SELF_SIGNED_CERT_IN_CHAIN') != -1) {
  235. console.error('Error making request, SELF_SIGNED_CERT_IN_CHAIN. Please read https://github.com/Medium/phantomjs#i-am-behind-a-corporate-proxy-that-uses-self-signed-ssl-certificates-to-intercept-encrypted-traffic')
  236. exit(1)
  237. } else if (error) {
  238. console.error('Error making request.\n' + error.stack + '\n\n' +
  239. 'Please report this full log at https://github.com/Medium/phantomjs')
  240. exit(1)
  241. } else {
  242. console.error('Something unexpected happened, please report this full ' +
  243. 'log at https://github.com/Medium/phantomjs')
  244. exit(1)
  245. }
  246. })).on('progress', function (state) {
  247. if (!bar) {
  248. bar = new progress(' [:bar] :percent :etas', {total: state.total, width: 40})
  249. }
  250. bar.curr = state.received
  251. bar.tick(0)
  252. })
  253. return deferred.promise
  254. }
  255. function extractDownload(filePath) {
  256. var deferred = kew.defer()
  257. // extract to a unique directory in case multiple processes are
  258. // installing and extracting at once
  259. var extractedPath = filePath + '-extract-' + Date.now()
  260. var options = {cwd: extractedPath}
  261. fs.mkdirsSync(extractedPath, '0777')
  262. // Make double sure we have 0777 permissions; some operating systems
  263. // default umask does not allow write by default.
  264. fs.chmodSync(extractedPath, '0777')
  265. if (filePath.substr(-4) === '.zip') {
  266. console.log('Extracting zip contents')
  267. try {
  268. var zip = new AdmZip(filePath)
  269. zip.extractAllTo(extractedPath, true)
  270. deferred.resolve(extractedPath)
  271. } catch (err) {
  272. console.error('Error extracting zip')
  273. deferred.reject(err)
  274. }
  275. } else {
  276. console.log('Extracting tar contents (via spawned process)')
  277. cp.execFile('tar', ['jxf', filePath], options, function (err, stdout, stderr) {
  278. if (err) {
  279. console.error('Error extracting archive')
  280. deferred.reject(err)
  281. } else {
  282. deferred.resolve(extractedPath)
  283. }
  284. })
  285. }
  286. return deferred.promise
  287. }
  288. function copyIntoPlace(extractedPath, targetPath) {
  289. console.log('Removing', targetPath)
  290. return kew.nfcall(fs.remove, targetPath).then(function () {
  291. // Look for the extracted directory, so we can rename it.
  292. var files = fs.readdirSync(extractedPath)
  293. for (var i = 0; i < files.length; i++) {
  294. var file = path.join(extractedPath, files[i])
  295. if (fs.statSync(file).isDirectory() && file.indexOf(helper.version) != -1) {
  296. console.log('Copying extracted folder', file, '->', targetPath)
  297. return kew.nfcall(fs.move, file, targetPath)
  298. }
  299. }
  300. console.log('Could not find extracted file', files)
  301. throw new Error('Could not find extracted file')
  302. })
  303. }