summaryrefslogtreecommitdiff
path: root/deps/npm/node_modules/bin-links/index.js
blob: 5f867554752231d3fb9083ca6676246efd856130 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
'use strict'

const path = require('path')
const fs = require('graceful-fs')
const BB = require('bluebird')
const linkIfExists = BB.promisify(require('gentle-fs').linkIfExists)
const cmdShimIfExists = BB.promisify(require('cmd-shim').ifExists)
const open = BB.promisify(fs.open)
const close = BB.promisify(fs.close)
const read = BB.promisify(fs.read, {multiArgs: true})
const chmod = BB.promisify(fs.chmod)
const readFile = BB.promisify(fs.readFile)
const writeFileAtomic = BB.promisify(require('write-file-atomic'))

module.exports = BB.promisify(binLinks)

function binLinks (pkg, folder, global, opts, cb) {
  // if it's global, and folder is in {prefix}/node_modules,
  // then bins are in {prefix}/bin
  // otherwise, then bins are in folder/../.bin
  var parent = pkg.name && pkg.name[0] === '@' ? path.dirname(path.dirname(folder)) : path.dirname(folder)
  var gnm = global && opts.globalDir
  var gtop = parent === gnm

  opts.log.info('linkStuff', opts.pkgId)
  opts.log.silly('linkStuff', opts.pkgId, 'has', parent, 'as its parent node_modules')
  if (global) opts.log.silly('linkStuff', opts.pkgId, 'is part of a global install')
  if (gnm) opts.log.silly('linkStuff', opts.pkgId, 'is installed into a global node_modules')
  if (gtop) opts.log.silly('linkStuff', opts.pkgId, 'is installed into the top-level global node_modules')

  return BB.join(
    linkBins(pkg, folder, parent, gtop, opts),
    linkMans(pkg, folder, parent, gtop, opts)
  ).asCallback(cb)
}

function isHashbangFile (file) {
  return open(file, 'r').then(fileHandle => {
    return read(fileHandle, Buffer.alloc(2), 0, 2, 0).spread((_, buf) => {
      if (!hasHashbang(buf)) return []
      return read(fileHandle, Buffer.alloc(2048), 0, 2048, 0)
    }).spread((_, buf) => buf && hasCR(buf), () => false)
      .finally(() => close(fileHandle))
  }).catch(() => false)
}

function hasHashbang (buf) {
  const str = buf.toString()
  return str.slice(0, 2) === '#!'
}

function hasCR (buf) {
  return /^#![^\n]+\r\n/.test(buf)
}

function dos2Unix (file) {
  return readFile(file, 'utf8').then(content => {
    return writeFileAtomic(file, content.replace(/^(#![^\n]+)\r\n/, '$1\n'))
  })
}

function getLinkOpts (opts, gently) {
  return Object.assign({}, opts, { gently: gently })
}

function linkBins (pkg, folder, parent, gtop, opts) {
  if (!pkg.bin || (!gtop && path.basename(parent) !== 'node_modules')) {
    return
  }
  var linkOpts = getLinkOpts(opts, gtop && folder)
  var execMode = parseInt('0777', 8) & (~opts.umask)
  var binRoot = gtop ? opts.globalBin
                     : path.resolve(parent, '.bin')
  opts.log.verbose('linkBins', [pkg.bin, binRoot, gtop])

  return BB.map(Object.keys(pkg.bin), bin => {
    var dest = path.resolve(binRoot, bin)
    var src = path.resolve(folder, pkg.bin[bin])

    return linkBin(src, dest, linkOpts).then(() => {
      // bins should always be executable.
      // XXX skip chmod on windows?
      return chmod(src, execMode)
    }).then(() => {
      return isHashbangFile(src)
    }).then(isHashbang => {
      if (!isHashbang) return
      opts.log.silly('linkBins', 'Converting line endings of hashbang file:', src)
      return dos2Unix(src)
    }).then(() => {
      if (!gtop) return
      var dest = path.resolve(binRoot, bin)
      var out = opts.parseable
              ? dest + '::' + src + ':BINFILE'
              : dest + ' -> ' + src

      if (!opts.json && !opts.parseable) {
        opts.log.clearProgress()
        console.log(out)
        opts.log.showProgress()
      }
    }).catch(err => {
      if (err.code === 'ENOENT' && opts.ignoreScripts) return
      throw err
    })
  })
}

function linkBin (from, to, opts) {
  if (process.platform !== 'win32') {
    return linkIfExists(from, to, opts)
  } else {
    return cmdShimIfExists(from, to)
  }
}

function linkMans (pkg, folder, parent, gtop, opts) {
  if (!pkg.man || !gtop || process.platform === 'win32') return

  var manRoot = path.resolve(opts.prefix, 'share', 'man')
  opts.log.verbose('linkMans', 'man files are', pkg.man, 'in', manRoot)

  // make sure that the mans are unique.
  // otherwise, if there are dupes, it'll fail with EEXIST
  var set = pkg.man.reduce(function (acc, man) {
    acc[path.basename(man)] = man
    return acc
  }, {})
  var manpages = pkg.man.filter(function (man) {
    return set[path.basename(man)] === man
  })

  return BB.map(manpages, man => {
    if (typeof man !== 'string') return
    opts.log.silly('linkMans', 'preparing to link', man)
    var parseMan = man.match(/(.*\.([0-9]+)(\.gz)?)$/)
    if (!parseMan) {
      throw new Error(
        man + ' is not a valid name for a man file.  ' +
        'Man files must end with a number, ' +
        'and optionally a .gz suffix if they are compressed.'
      )
    }

    var stem = parseMan[1]
    var sxn = parseMan[2]
    var bn = path.basename(stem)
    var manSrc = path.resolve(folder, man)
    var manDest = path.join(manRoot, 'man' + sxn, bn)

    return linkIfExists(manSrc, manDest, getLinkOpts(opts, gtop && folder))
  })
}