codekingpro/portable-devtools
114k
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 }, () => 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 