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