UNPKG

13.8 kBJavaScriptView Raw
1// Copyright 2012 The Obvious Corporation.
2
3/*
4 * This simply fetches the right version of phantom for the current platform.
5 */
6
7'use strict'
8
9var requestProgress = require('request-progress')
10var progress = require('progress')
11var extractZip = require('extract-zip')
12var cp = require('child_process')
13var fs = require('fs-extra')
14var helper = require('./lib/phantomjs')
15var kew = require('kew')
16var path = require('path')
17var request = require('request')
18var url = require('url')
19var util = require('./lib/util')
20var which = require('which')
21var os = require('os')
22
23var originalPath = process.env.PATH
24
25var checkPhantomjsVersion = util.checkPhantomjsVersion
26var getTargetPlatform = util.getTargetPlatform
27var getTargetArch = util.getTargetArch
28var getDownloadSpec = util.getDownloadSpec
29var findValidPhantomJsBinary = util.findValidPhantomJsBinary
30var verifyChecksum = util.verifyChecksum
31var writeLocationFile = util.writeLocationFile
32
33// If the process exits without going through exit(), then we did not complete.
34var validExit = false
35
36process.on('exit', function () {
37 if (!validExit) {
38 console.log('Install exited unexpectedly')
39 exit(1)
40 }
41})
42
43// NPM adds bin directories to the path, which will cause `which` to find the
44// bin for this package not the actual phantomjs bin. Also help out people who
45// put ./bin on their path
46process.env.PATH = helper.cleanPath(originalPath)
47
48var libPath = path.join(__dirname, 'lib')
49var pkgPath = path.join(libPath, 'phantom')
50var phantomPath = null
51
52// If the user manually installed PhantomJS, we want
53// to use the existing version.
54//
55// Do not re-use a manually-installed PhantomJS with
56// a different version.
57//
58// Do not re-use an npm-installed PhantomJS, because
59// that can lead to weird circular dependencies between
60// local versions and global versions.
61// https://github.com/Obvious/phantomjs/issues/85
62// https://github.com/Medium/phantomjs/pull/184
63kew.resolve(true)
64 .then(tryPhantomjsInLib)
65 .then(tryPhantomjsOnPath)
66 .then(downloadPhantomjs)
67 .then(extractDownload)
68 .then(function (extractedPath) {
69 return copyIntoPlace(extractedPath, pkgPath)
70 })
71 .then(function () {
72 var location = getTargetPlatform() === 'win32' ?
73 path.join(pkgPath, 'bin', 'phantomjs.exe') :
74 path.join(pkgPath, 'bin' ,'phantomjs')
75
76 try {
77 // Ensure executable is executable by all users
78 fs.chmodSync(location, '755')
79 } catch (err) {
80 if (err.code == 'ENOENT') {
81 console.error('chmod failed: phantomjs was not successfully copied to', location)
82 exit(1)
83 }
84 throw err
85 }
86
87 var relativeLocation = path.relative(libPath, location)
88 writeLocationFile(relativeLocation)
89
90 console.log('Done. Phantomjs binary available at', location)
91 exit(0)
92 })
93 .fail(function (err) {
94 console.error('Phantom installation failed', err, err.stack)
95 exit(1)
96 })
97
98function exit(code) {
99 validExit = true
100 process.env.PATH = originalPath
101 process.exit(code || 0)
102}
103
104
105function findSuitableTempDirectory() {
106 var now = Date.now()
107 var candidateTmpDirs = [
108 process.env.npm_config_tmp,
109 os.tmpdir(),
110 path.join(process.cwd(), 'tmp')
111 ]
112
113 for (var i = 0; i < candidateTmpDirs.length; i++) {
114 var candidatePath = candidateTmpDirs[i]
115 if (!candidatePath) continue
116
117 try {
118 candidatePath = path.join(path.resolve(candidatePath), 'phantomjs')
119 fs.mkdirsSync(candidatePath, '0777')
120 // Make double sure we have 0777 permissions; some operating systems
121 // default umask does not allow write by default.
122 fs.chmodSync(candidatePath, '0777')
123 var testFile = path.join(candidatePath, now + '.tmp')
124 fs.writeFileSync(testFile, 'test')
125 fs.unlinkSync(testFile)
126 return candidatePath
127 } catch (e) {
128 console.log(candidatePath, 'is not writable:', e.message)
129 }
130 }
131
132 console.error('Can not find a writable tmp directory, please report issue ' +
133 'on https://github.com/Medium/phantomjs/issues with as much ' +
134 'information as possible.')
135 exit(1)
136}
137
138
139function getRequestOptions() {
140 var strictSSL = !!process.env.npm_config_strict_ssl
141 if (process.version == 'v0.10.34') {
142 console.log('Node v0.10.34 detected, turning off strict ssl due to https://github.com/joyent/node/issues/8894')
143 strictSSL = false
144 }
145
146 var options = {
147 uri: getDownloadUrl(),
148 encoding: null, // Get response as a buffer
149 followRedirect: true, // The default download path redirects to a CDN URL.
150 headers: {},
151 strictSSL: strictSSL
152 }
153
154 var proxyUrl = process.env.npm_config_https_proxy ||
155 process.env.npm_config_http_proxy ||
156 process.env.npm_config_proxy
157 if (proxyUrl) {
158
159 // Print using proxy
160 var proxy = url.parse(proxyUrl)
161 if (proxy.auth) {
162 // Mask password
163 proxy.auth = proxy.auth.replace(/:.*$/, ':******')
164 }
165 console.log('Using proxy ' + url.format(proxy))
166
167 // Enable proxy
168 options.proxy = proxyUrl
169 }
170
171 // Use the user-agent string from the npm config
172 options.headers['User-Agent'] = process.env.npm_config_user_agent
173
174 // Use certificate authority settings from npm
175 var ca = process.env.npm_config_ca
176 if (!ca && process.env.npm_config_cafile) {
177 try {
178 ca = fs.readFileSync(process.env.npm_config_cafile, {encoding: 'utf8'})
179 .split(/\n(?=-----BEGIN CERTIFICATE-----)/g)
180
181 // Comments at the beginning of the file result in the first
182 // item not containing a certificate - in this case the
183 // download will fail
184 if (ca.length > 0 && !/-----BEGIN CERTIFICATE-----/.test(ca[0])) {
185 ca.shift()
186 }
187
188 } catch (e) {
189 console.error('Could not read cafile', process.env.npm_config_cafile, e)
190 }
191 }
192
193 if (ca) {
194 console.log('Using npmconf ca')
195 options.agentOptions = {
196 ca: ca
197 }
198 options.ca = ca
199 }
200
201 return options
202}
203
204function handleRequestError(error) {
205 if (error && error.stack && error.stack.indexOf('SELF_SIGNED_CERT_IN_CHAIN') != -1) {
206 console.error('Error making request, SELF_SIGNED_CERT_IN_CHAIN. ' +
207 'Please read https://github.com/Medium/phantomjs#i-am-behind-a-corporate-proxy-that-uses-self-signed-ssl-certificates-to-intercept-encrypted-traffic')
208 exit(1)
209 } else if (error) {
210 console.error('Error making request.\n' + error.stack + '\n\n' +
211 'Please report this full log at https://github.com/Medium/phantomjs')
212 exit(1)
213 } else {
214 console.error('Something unexpected happened, please report this full ' +
215 'log at https://github.com/Medium/phantomjs')
216 exit(1)
217 }
218}
219
220function requestBinary(requestOptions, filePath) {
221 var deferred = kew.defer()
222
223 var writePath = filePath + '-download-' + Date.now()
224
225 console.log('Receiving...')
226 var bar = null
227 requestProgress(request(requestOptions, function (error, response, body) {
228 console.log('')
229 if (!error && response.statusCode === 200) {
230 fs.writeFileSync(writePath, body)
231 console.log('Received ' + Math.floor(body.length / 1024) + 'K total.')
232 fs.renameSync(writePath, filePath)
233 deferred.resolve(filePath)
234
235 } else if (response) {
236 console.error('Error requesting archive.\n' +
237 'Status: ' + response.statusCode + '\n' +
238 'Request options: ' + JSON.stringify(requestOptions, null, 2) + '\n' +
239 'Response headers: ' + JSON.stringify(response.headers, null, 2) + '\n' +
240 'Make sure your network and proxy settings are correct.\n\n' +
241 'If you continue to have issues, please report this full log at ' +
242 'https://github.com/Medium/phantomjs')
243 exit(1)
244 } else {
245 handleRequestError(error)
246 }
247 })).on('progress', function (state) {
248 try {
249 if (!bar) {
250 bar = new progress(' [:bar] :percent', {total: state.size.total, width: 40})
251 }
252 bar.curr = state.size.transferred
253 bar.tick()
254 } catch (e) {
255 // It doesn't really matter if the progress bar doesn't update.
256 }
257 })
258 .on('error', handleRequestError)
259
260 return deferred.promise
261}
262
263
264function extractDownload(filePath) {
265 var deferred = kew.defer()
266 // extract to a unique directory in case multiple processes are
267 // installing and extracting at once
268 var extractedPath = filePath + '-extract-' + Date.now()
269 var options = {cwd: extractedPath}
270
271 fs.mkdirsSync(extractedPath, '0777')
272 // Make double sure we have 0777 permissions; some operating systems
273 // default umask does not allow write by default.
274 fs.chmodSync(extractedPath, '0777')
275
276 if (filePath.substr(-4) === '.zip') {
277 console.log('Extracting zip contents')
278 extractZip(path.resolve(filePath), {dir: extractedPath}, function(err) {
279 if (err) {
280 console.error('Error extracting zip')
281 deferred.reject(err)
282 } else {
283 deferred.resolve(extractedPath)
284 }
285 })
286
287 } else {
288 console.log('Extracting tar contents (via spawned process)')
289 cp.execFile('tar', ['jxf', path.resolve(filePath)], options, function (err) {
290 if (err) {
291 console.error('Error extracting archive')
292 deferred.reject(err)
293 } else {
294 deferred.resolve(extractedPath)
295 }
296 })
297 }
298 return deferred.promise
299}
300
301
302function copyIntoPlace(extractedPath, targetPath) {
303 console.log('Removing', targetPath)
304 return kew.nfcall(fs.remove, targetPath).then(function () {
305 // Look for the extracted directory, so we can rename it.
306 var files = fs.readdirSync(extractedPath)
307 for (var i = 0; i < files.length; i++) {
308 var file = path.join(extractedPath, files[i])
309 if (fs.statSync(file).isDirectory() && file.indexOf(helper.version) != -1) {
310 console.log('Copying extracted folder', file, '->', targetPath)
311 return kew.nfcall(fs.move, file, targetPath)
312 }
313 }
314
315 console.log('Could not find extracted file', files)
316 throw new Error('Could not find extracted file')
317 })
318}
319
320/**
321 * Check to see if the binary in lib is OK to use. If successful, exit the process.
322 */
323function tryPhantomjsInLib() {
324 return kew.fcall(function () {
325 return findValidPhantomJsBinary(path.resolve(__dirname, './lib/location.js'))
326 }).then(function (binaryLocation) {
327 if (binaryLocation) {
328 console.log('PhantomJS is previously installed at', binaryLocation)
329 exit(0)
330 }
331 }).fail(function () {
332 // silently swallow any errors
333 })
334}
335
336/**
337 * Check to see if the binary on PATH is OK to use. If successful, exit the process.
338 */
339function tryPhantomjsOnPath() {
340 if (getTargetPlatform() != process.platform || getTargetArch() != process.arch) {
341 console.log('Building for target platform ' + getTargetPlatform() + '/' + getTargetArch() +
342 '. Skipping PATH search')
343 return kew.resolve(false)
344 }
345
346 return kew.nfcall(which, 'phantomjs')
347 .then(function (result) {
348 phantomPath = result
349 console.log('Considering PhantomJS found at', phantomPath)
350
351 // Horrible hack to avoid problems during global install. We check to see if
352 // the file `which` found is our own bin script.
353 if (phantomPath.indexOf(path.join('npm', 'phantomjs')) !== -1) {
354 console.log('Looks like an `npm install -g` on windows; skipping installed version.')
355 return
356 }
357
358 var contents = fs.readFileSync(phantomPath, 'utf8')
359 if (/NPM_INSTALL_MARKER/.test(contents)) {
360 console.log('Looks like an `npm install -g`')
361
362 var phantomLibPath = path.resolve(fs.realpathSync(phantomPath), '../../lib/location')
363 return findValidPhantomJsBinary(phantomLibPath)
364 .then(function (binaryLocation) {
365 if (binaryLocation) {
366 writeLocationFile(binaryLocation)
367 console.log('PhantomJS linked at', phantomLibPath)
368 exit(0)
369 }
370 console.log('Could not link global install, skipping...')
371 })
372 } else {
373 return checkPhantomjsVersion(phantomPath).then(function (matches) {
374 if (matches) {
375 writeLocationFile(phantomPath)
376 console.log('PhantomJS is already installed on PATH at', phantomPath)
377 exit(0)
378 }
379 })
380 }
381 }, function () {
382 console.log('PhantomJS not found on PATH')
383 })
384 .fail(function (err) {
385 console.error('Error checking path, continuing', err)
386 return false
387 })
388}
389
390/**
391 * @return {?string} Get the download URL for phantomjs.
392 * May return null if no download url exists.
393 */
394function getDownloadUrl() {
395 var spec = getDownloadSpec()
396 return spec && spec.url
397}
398
399/**
400 * Download phantomjs, reusing the existing copy on disk if available.
401 * Exits immediately if there is no binary to download.
402 * @return {Promise.<string>} The path to the downloaded file.
403 */
404function downloadPhantomjs() {
405 var downloadSpec = getDownloadSpec()
406 if (!downloadSpec) {
407 console.error(
408 'Unexpected platform or architecture: ' + getTargetPlatform() + '/' + getTargetArch() + '\n' +
409 'It seems there is no binary available for your platform/architecture\n' +
410 'Try to install PhantomJS globally')
411 exit(1)
412 }
413
414 var downloadUrl = downloadSpec.url
415 var downloadedFile
416
417 return kew.fcall(function () {
418 // Can't use a global version so start a download.
419 var tmpPath = findSuitableTempDirectory()
420 var fileName = downloadUrl.split('/').pop()
421 downloadedFile = path.join(tmpPath, fileName)
422
423 if (fs.existsSync(downloadedFile)) {
424 console.log('Download already available at', downloadedFile)
425 return verifyChecksum(downloadedFile, downloadSpec.checksum)
426 }
427 return false
428 }).then(function (verified) {
429 if (verified) {
430 return downloadedFile
431 }
432
433 // Start the install.
434 console.log('Downloading', downloadUrl)
435 console.log('Saving to', downloadedFile)
436 return requestBinary(getRequestOptions(), downloadedFile)
437 })
438}