• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// On windows, create a .cmd file.
2// Read the #! in the file to see what it uses.  The vast majority
3// of the time, this will be either:
4// "#!/usr/bin/env <prog> <args...>"
5// or:
6// "#!<prog> <args...>"
7//
8// Write a binroot/pkg.bin + ".cmd" file that has this line in it:
9// @<prog> <args...> %dp0%<target> %*
10
11const {
12  chmod,
13  mkdir,
14  readFile,
15  stat,
16  unlink,
17  writeFile,
18} = require('fs/promises')
19
20const { dirname, relative } = require('path')
21const toBatchSyntax = require('./to-batch-syntax')
22// linting disabled because this regex is really long
23// eslint-disable-next-line max-len
24const shebangExpr = /^#!\s*(?:\/usr\/bin\/env\s+(?:-S\s+)?((?:[^ \t=]+=[^ \t=]+\s+)*))?([^ \t]+)(.*)$/
25
26const cmdShimIfExists = (from, to) =>
27  stat(from).then(() => cmdShim(from, to), () => {})
28
29// Try to unlink, but ignore errors.
30// Any problems will surface later.
31const rm = path => unlink(path).catch(() => {})
32
33const cmdShim = (from, to) =>
34  stat(from).then(() => cmdShim_(from, to))
35
36const cmdShim_ = (from, to) => Promise.all([
37  rm(to),
38  rm(to + '.cmd'),
39  rm(to + '.ps1'),
40]).then(() => writeShim(from, to))
41
42const writeShim = (from, to) =>
43  // make a cmd file and a sh script
44  // First, check if the bin is a #! of some sort.
45  // If not, then assume it's something that'll be compiled, or some other
46  // sort of script, and just call it directly.
47  mkdir(dirname(to), { recursive: true })
48    .then(() => readFile(from, 'utf8'))
49    .then(data => {
50      const firstLine = data.trim().split(/\r*\n/)[0]
51      const shebang = firstLine.match(shebangExpr)
52      if (!shebang) {
53        return writeShim_(from, to)
54      }
55      const vars = shebang[1] || ''
56      const prog = shebang[2]
57      const args = shebang[3] || ''
58      return writeShim_(from, to, prog, args, vars)
59    }, er => writeShim_(from, to))
60
61const writeShim_ = (from, to, prog, args, variables) => {
62  let shTarget = relative(dirname(to), from)
63  let target = shTarget.split('/').join('\\')
64  let longProg
65  let shProg = prog && prog.split('\\').join('/')
66  let shLongProg
67  let pwshProg = shProg && `"${shProg}$exe"`
68  let pwshLongProg
69  shTarget = shTarget.split('\\').join('/')
70  args = args || ''
71  variables = variables || ''
72  if (!prog) {
73    prog = `"%dp0%\\${target}"`
74    shProg = `"$basedir/${shTarget}"`
75    pwshProg = shProg
76    args = ''
77    target = ''
78    shTarget = ''
79  } else {
80    longProg = `"%dp0%\\${prog}.exe"`
81    shLongProg = `"$basedir/${prog}"`
82    pwshLongProg = `"$basedir/${prog}$exe"`
83    target = `"%dp0%\\${target}"`
84    shTarget = `"$basedir/${shTarget}"`
85  }
86
87  // Subroutine trick to fix https://github.com/npm/cmd-shim/issues/10
88  // and https://github.com/npm/cli/issues/969
89  const head = '@ECHO off\r\n' +
90    'GOTO start\r\n' +
91    ':find_dp0\r\n' +
92    'SET dp0=%~dp0\r\n' +
93    'EXIT /b\r\n' +
94    ':start\r\n' +
95    'SETLOCAL\r\n' +
96    'CALL :find_dp0\r\n'
97
98  let cmd
99  if (longProg) {
100    shLongProg = shLongProg.trim()
101    args = args.trim()
102    const variablesBatch = toBatchSyntax.convertToSetCommands(variables)
103    cmd = head
104        + variablesBatch
105        + '\r\n'
106        + `IF EXIST ${longProg} (\r\n`
107        + `  SET "_prog=${longProg.replace(/(^")|("$)/g, '')}"\r\n`
108        + ') ELSE (\r\n'
109        + `  SET "_prog=${prog.replace(/(^")|("$)/g, '')}"\r\n`
110        + '  SET PATHEXT=%PATHEXT:;.JS;=;%\r\n'
111        + ')\r\n'
112        + '\r\n'
113        // prevent "Terminate Batch Job? (Y/n)" message
114        // https://github.com/npm/cli/issues/969#issuecomment-737496588
115        + 'endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & '
116        + `"%_prog%" ${args} ${target} %*\r\n`
117  } else {
118    cmd = `${head}${prog} ${args} ${target} %*\r\n`
119  }
120
121  // #!/bin/sh
122  // basedir=`dirname "$0"`
123  //
124  // case `uname` in
125  //     *CYGWIN*|*MINGW*|*MSYS*)
126  //       if command -v cygpath > /dev/null 2>&1; then
127  //           basedir=`cygpath -w "$basedir"`
128  //       fi
129  //     ;;
130  // esac
131  //
132  // if [ -x "$basedir/node.exe" ]; then
133  //   exec "$basedir/node.exe" "$basedir/node_modules/npm/bin/npm-cli.js" "$@"
134  // else
135  //   exec node "$basedir/node_modules/npm/bin/npm-cli.js" "$@"
136  // fi
137
138  let sh = '#!/bin/sh\n'
139
140  sh = sh
141      + `basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')")\n`
142      + '\n'
143      + 'case `uname` in\n'
144      + '    *CYGWIN*|*MINGW*|*MSYS*)\n'
145      + '        if command -v cygpath > /dev/null 2>&1; then\n'
146      + '            basedir=`cygpath -w "$basedir"`\n'
147      + '        fi\n'
148      + '    ;;\n'
149      + 'esac\n'
150      + '\n'
151
152  if (shLongProg) {
153    sh = sh
154       + `if [ -x ${shLongProg} ]; then\n`
155       + `  exec ${variables}${shLongProg} ${args} ${shTarget} "$@"\n`
156       + 'else \n'
157       + `  exec ${variables}${shProg} ${args} ${shTarget} "$@"\n`
158       + 'fi\n'
159  } else {
160    sh = sh
161       + `exec ${shProg} ${args} ${shTarget} "$@"\n`
162  }
163
164  // #!/usr/bin/env pwsh
165  // $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
166  //
167  // $ret=0
168  // $exe = ""
169  // if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
170  //   # Fix case when both the Windows and Linux builds of Node
171  //   # are installed in the same directory
172  //   $exe = ".exe"
173  // }
174  // if (Test-Path "$basedir/node") {
175  //   # Suport pipeline input
176  //   if ($MyInvocation.ExpectingInput) {
177  //     input | & "$basedir/node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
178  //   } else {
179  //     & "$basedir/node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
180  //   }
181  //   $ret=$LASTEXITCODE
182  // } else {
183  //   # Support pipeline input
184  //   if ($MyInvocation.ExpectingInput) {
185  //     $input | & "node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
186  //   } else {
187  //     & "node$exe" "$basedir/node_modules/npm/bin/npm-cli.js" $args
188  //   }
189  //   $ret=$LASTEXITCODE
190  // }
191  // exit $ret
192  let pwsh = '#!/usr/bin/env pwsh\n'
193           + '$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent\n'
194           + '\n'
195           + '$exe=""\n'
196           + 'if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {\n'
197           + '  # Fix case when both the Windows and Linux builds of Node\n'
198           + '  # are installed in the same directory\n'
199           + '  $exe=".exe"\n'
200           + '}\n'
201  if (shLongProg) {
202    pwsh = pwsh
203         + '$ret=0\n'
204         + `if (Test-Path ${pwshLongProg}) {\n`
205         + '  # Support pipeline input\n'
206         + '  if ($MyInvocation.ExpectingInput) {\n'
207         + `    $input | & ${pwshLongProg} ${args} ${shTarget} $args\n`
208         + '  } else {\n'
209         + `    & ${pwshLongProg} ${args} ${shTarget} $args\n`
210         + '  }\n'
211         + '  $ret=$LASTEXITCODE\n'
212         + '} else {\n'
213         + '  # Support pipeline input\n'
214         + '  if ($MyInvocation.ExpectingInput) {\n'
215         + `    $input | & ${pwshProg} ${args} ${shTarget} $args\n`
216         + '  } else {\n'
217         + `    & ${pwshProg} ${args} ${shTarget} $args\n`
218         + '  }\n'
219         + '  $ret=$LASTEXITCODE\n'
220         + '}\n'
221         + 'exit $ret\n'
222  } else {
223    pwsh = pwsh
224         + '# Support pipeline input\n'
225         + 'if ($MyInvocation.ExpectingInput) {\n'
226         + `  $input | & ${pwshProg} ${args} ${shTarget} $args\n`
227         + '} else {\n'
228         + `  & ${pwshProg} ${args} ${shTarget} $args\n`
229         + '}\n'
230         + 'exit $LASTEXITCODE\n'
231  }
232
233  return Promise.all([
234    writeFile(to + '.ps1', pwsh, 'utf8'),
235    writeFile(to + '.cmd', cmd, 'utf8'),
236    writeFile(to, sh, 'utf8'),
237  ]).then(() => chmodShim(to))
238}
239
240const chmodShim = to => Promise.all([
241  chmod(to, 0o755),
242  chmod(to + '.cmd', 0o755),
243  chmod(to + '.ps1', 0o755),
244])
245
246module.exports = cmdShim
247cmdShim.ifExists = cmdShimIfExists
248