Skip to content

Commit e69be2a

Browse files
isaacsruyadorno
authored andcommitted
Get correct npm prefix on all Windows unix shells
1. Set the shebang to /usr/bin/env bash instead of /bin/sh (which might be dash or some other shell) 2. Use Unix-style line endings, not Windows-style (Cygwin accepts either, but mingw bash sometimes objects, and WSL bash always does) 3. Test against paths using wslpath if available, but still pass win32 paths to node.exe, since it is a Windows binary that only knows how to handle Windows paths. This makes npm as installed by the Node.js Windows MSI installer behave properly under WSL, Cygwin, MINGW Git Bash, and the internal MINGW Git Bash when posix CLI utilities are exposed to the cmd.exe shell. The test is not quite as comprehensive as I'd like. It runs on the various Windows bash implementations if they are found in their expected locations, skipping any that are not installed. Short of shipping mingw, cygwin, and wsl as test fixtures, I'm not sure how we could do much better, however. At least, we can use this test to assist debug and catch issues on Windows machines (ours or users who report problems). PR-URL: #2789 Credit: @isaacs Close: #2789 Reviewed-by: @nlf
1 parent 4a5dd3a commit e69be2a

File tree

6 files changed

+198
-49
lines changed

6 files changed

+198
-49
lines changed

bin/npm

+26-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
#!/bin/sh
1+
#!/usr/bin/env bash
22
(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix
33

44
basedir=`dirname "$0"`
55

66
case `uname` in
7-
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
7+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
88
esac
99

1010
NODE_EXE="$basedir/node.exe"
@@ -15,23 +15,30 @@ if ! [ -x "$NODE_EXE" ]; then
1515
NODE_EXE=node
1616
fi
1717

18-
NPM_CLI_JS="$basedir/node_modules/npm/bin/npm-cli.js"
18+
# this path is passed to node.exe, so it needs to match whatever
19+
# kind of paths Node.js thinks it's using, typically win32 paths.
20+
CLI_BASEDIR="$("$NODE_EXE" -p 'require("path").dirname(process.execPath)')"
21+
NPM_CLI_JS="$CLI_BASEDIR/node_modules/npm/bin/npm-cli.js"
1922

20-
case `uname` in
21-
*MINGW*)
22-
NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g`
23-
NPM_PREFIX_NPM_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npm-cli.js"
24-
if [ -f "$NPM_PREFIX_NPM_CLI_JS" ]; then
25-
NPM_CLI_JS="$NPM_PREFIX_NPM_CLI_JS"
26-
fi
27-
;;
28-
*CYGWIN*)
29-
NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g`
30-
NPM_PREFIX_NPM_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npm-cli.js"
31-
if [ -f "$NPM_PREFIX_NPM_CLI_JS" ]; then
32-
NPM_CLI_JS="$NPM_PREFIX_NPM_CLI_JS"
33-
fi
34-
;;
35-
esac
23+
NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g`
24+
if [ $? -ne 0 ]; then
25+
# if this didn't work, then everything else below will fail
26+
echo "Could not determine Node.js install directory" >&2
27+
exit 1
28+
fi
29+
NPM_PREFIX_NPM_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npm-cli.js"
30+
31+
# a path that will fail -f test on any posix bash
32+
NPM_WSL_PATH="/.."
33+
34+
# WSL can run Windows binaries, so we have to give it the win32 path
35+
# however, WSL bash tests against posix paths, so we need to construct that
36+
# to know if npm is installed globally.
37+
if [ `uname` = 'Linux' ] && type wslpath &>/dev/null ; then
38+
NPM_WSL_PATH=`wslpath "$NPM_PREFIX_NPM_CLI_JS"`
39+
fi
40+
if [ -f "$NPM_PREFIX_NPM_CLI_JS" ] || [ -f "$NPM_WSL_PATH" ]; then
41+
NPM_CLI_JS="$NPM_PREFIX_NPM_CLI_JS"
42+
fi
3643

3744
"$NODE_EXE" "$NPM_CLI_JS" "$@"

bin/npx

+27-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/bin/sh
1+
#!/usr/bin/env bash
22

33
# This is used by the Node.js installer, which expects the cygwin/mingw
44
# shell script to already be present in the npm dependency folder.
@@ -8,32 +8,38 @@
88
basedir=`dirname "$0"`
99

1010
case `uname` in
11-
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
11+
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
1212
esac
1313

1414
NODE_EXE="$basedir/node.exe"
1515
if ! [ -x "$NODE_EXE" ]; then
1616
NODE_EXE=node
1717
fi
1818

19-
NPM_CLI_JS="$basedir/node_modules/npm/bin/npm-cli.js"
20-
NPX_CLI_JS="$basedir/node_modules/npm/bin/npx-cli.js"
21-
22-
case `uname` in
23-
*MINGW*)
24-
NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g`
25-
NPM_PREFIX_NPX_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npx-cli.js"
26-
if [ -f "$NPM_PREFIX_NPX_CLI_JS" ]; then
27-
NPX_CLI_JS="$NPM_PREFIX_NPX_CLI_JS"
28-
fi
29-
;;
30-
*CYGWIN*)
31-
NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g`
32-
NPM_PREFIX_NPX_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npx-cli.js"
33-
if [ -f "$NPM_PREFIX_NPX_CLI_JS" ]; then
34-
NPX_CLI_JS="$NPM_PREFIX_NPX_CLI_JS"
35-
fi
36-
;;
37-
esac
19+
# these paths are passed to node.exe, so they need to match whatever
20+
# kind of paths Node.js thinks it's using, typically win32 paths.
21+
CLI_BASEDIR="$("$NODE_EXE" -p 'require("path").dirname(process.execPath)')"
22+
if [ $? -ne 0 ]; then
23+
# if this didn't work, then everything else below will fail
24+
echo "Could not determine Node.js install directory" >&2
25+
exit 1
26+
fi
27+
NPM_CLI_JS="$CLI_BASEDIR/node_modules/npm/bin/npm-cli.js"
28+
NPX_CLI_JS="$CLI_BASEDIR/node_modules/npm/bin/npx-cli.js"
29+
NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g`
30+
NPM_PREFIX_NPX_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npx-cli.js"
31+
32+
# a path that will fail -f test on any posix bash
33+
NPX_WSL_PATH="/.."
34+
35+
# WSL can run Windows binaries, so we have to give it the win32 path
36+
# however, WSL bash tests against posix paths, so we need to construct that
37+
# to know if npm is installed globally.
38+
if [ `uname` = 'Linux' ] && type wslpath &>/dev/null ; then
39+
NPX_WSL_PATH=`wslpath "$NPM_PREFIX_NPX_CLI_JS"`
40+
fi
41+
if [ -f "$NPM_PREFIX_NPX_CLI_JS" ] || [ -f "$NPX_WSL_PATH" ]; then
42+
NPX_CLI_JS="$NPM_PREFIX_NPX_CLI_JS"
43+
fi
3844

3945
"$NODE_EXE" "$NPX_CLI_JS" "$@"

test/bin/windows-shims.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
const t = require('tap')
2+
3+
if (process.platform !== 'win32') {
4+
t.plan(0, 'test only relevant on windows')
5+
process.exit(0)
6+
}
7+
8+
const has = path => {
9+
try {
10+
// If WSL is installed, it *has* a bash.exe, but it fails if
11+
// there is no distro installed, so we need to detect that.
12+
const result = spawnSync(path, ['-l', '-c', 'exit 0'])
13+
if (result.status === 0)
14+
return true
15+
else {
16+
// print whatever error we got
17+
throw result.error || Object.assign(new Error(String(result.stderr)), {
18+
code: result.status,
19+
})
20+
}
21+
} catch (er) {
22+
t.comment(`not installed: ${path}`, er)
23+
return false
24+
}
25+
}
26+
27+
const { version } = require('../../package.json')
28+
const spawn = require('@npmcli/promise-spawn')
29+
const { spawnSync } = require('child_process')
30+
const { resolve } = require('path')
31+
const { ProgramFiles, SystemRoot } = process.env
32+
const { readFileSync, chmodSync } = require('fs')
33+
const gitBash = resolve(ProgramFiles, 'Git', 'bin', 'bash.exe')
34+
const gitUsrBinBash = resolve(ProgramFiles, 'Git', 'usr', 'bin', 'bash.exe')
35+
const wslBash = resolve(SystemRoot, 'System32', 'bash.exe')
36+
const cygwinBash = resolve(SystemRoot, '/', 'cygwin64', 'bin', 'bash.exe')
37+
38+
const bashes = Object.entries({
39+
'wsl bash': wslBash,
40+
'git bash': gitBash,
41+
'git internal bash': gitUsrBinBash,
42+
'cygwin bash': cygwinBash,
43+
})
44+
45+
const npmShim = resolve(__dirname, '../../bin/npm')
46+
const npxShim = resolve(__dirname, '../../bin/npx')
47+
48+
const path = t.testdir({
49+
'node.exe': readFileSync(process.execPath),
50+
npm: readFileSync(npmShim),
51+
npx: readFileSync(npxShim),
52+
// simulate the state where one version of npm is installed
53+
// with node, but we should load the globally installed one
54+
'global-prefix': {
55+
node_modules: {
56+
npm: t.fixture('symlink', resolve(__dirname, '../..')),
57+
},
58+
},
59+
// put in a shim that ONLY prints the intended global prefix,
60+
// and should not be used for anything else.
61+
node_modules: {
62+
npm: {
63+
bin: {
64+
'npx-cli.js': `
65+
throw new Error('this should not be called')
66+
`,
67+
'npm-cli.js': `
68+
const assert = require('assert')
69+
const args = process.argv.slice(2)
70+
assert.equal(args[0], 'prefix')
71+
assert.equal(args[1], '-g')
72+
const { resolve } = require('path')
73+
console.log(resolve(__dirname, '../../../global-prefix'))
74+
`,
75+
},
76+
},
77+
},
78+
})
79+
chmodSync(resolve(path, 'npm'), 0o755)
80+
chmodSync(resolve(path, 'npx'), 0o755)
81+
82+
for (const [name, bash] of bashes) {
83+
if (!has(bash)) {
84+
t.skip(`${name} not installed`, { bin: bash, diagnostic: true })
85+
continue
86+
}
87+
88+
if (bash === cygwinBash && process.env.NYC_CONFIG) {
89+
t.skip('Cygwin does not play nicely with NYC, run without coverage')
90+
continue
91+
}
92+
93+
t.test(name, async t => {
94+
t.plan(2)
95+
t.test('npm', async t => {
96+
// only cygwin *requires* the -l, but the others are ok with it
97+
// don't hit the registry for the update check
98+
const args = ['-l', 'npm', 'help']
99+
100+
const result = await spawn(bash, args, {
101+
env: { PATH: path, npm_config_update_notifier: 'false' },
102+
cwd: path,
103+
stdioString: true,
104+
})
105+
t.match(result, {
106+
cmd: bash,
107+
args: ['-l', 'npm', 'help'],
108+
code: 0,
109+
signal: null,
110+
stderr: String,
111+
// should have loaded this instance of npm we symlinked in
112+
stdout: `npm@${version} ${resolve(__dirname, '../..')}`,
113+
})
114+
})
115+
116+
t.test('npx', async t => {
117+
const args = ['-l', 'npx', '--version']
118+
119+
const result = await spawn(bash, args, {
120+
env: { PATH: path, npm_config_update_notifier: 'false' },
121+
cwd: path,
122+
stdioString: true,
123+
})
124+
t.match(result, {
125+
cmd: bash,
126+
args: ['-l', 'npx', '--version'],
127+
code: 0,
128+
signal: null,
129+
stderr: String,
130+
// should have loaded this instance of npm we symlinked in
131+
stdout: version,
132+
})
133+
})
134+
})
135+
}

test/coverage-map.js

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ const coverageMap = (filename) => {
77
return glob.sync(`${dir}/**/*.js`)
88
.map(f => relative(process.cwd(), f))
99
}
10+
if (/windows-shims\.js$/.test(filename)) {
11+
// this one doesn't provide any coverage nyc can track
12+
return []
13+
}
1014
if (/^test\/(lib|bin)\//.test(filename))
1115
return filename.replace(/^test\//, '')
1216
return []

test/lib/doctor.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ test('node versions', t => {
487487
const dir = st.testdir({
488488
cache: {
489489
one: 'one',
490-
link: st.fixture('symlink', './one'),
490+
link: st.fixture('symlink', './baddir'),
491491
unreadable: 'unreadable',
492492
baddir: {},
493493
},

test/lib/npm.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const t = require('tap')
2-
const fs = require('fs')
32

43
// delete this so that we don't have configs from the fact that it
54
// is being run by 'npm test'
@@ -21,7 +20,7 @@ for (const env of Object.keys(process.env).filter(e => /^npm_/.test(e))) {
2120
delete process.env[env]
2221
}
2322

24-
const { resolve } = require('path')
23+
const { resolve, dirname } = require('path')
2524

2625
const actualPlatform = process.platform
2726

@@ -249,13 +248,11 @@ t.test('npm.load', t => {
249248
const node = actualPlatform === 'win32' ? 'node.exe' : 'node'
250249
const dir = t.testdir({
251250
'.npmrc': 'foo = bar',
251+
bin: t.fixture('symlink', dirname(process.execPath)),
252252
})
253253

254-
// create manually to set the 'file' option in windows
255-
fs.symlinkSync(process.execPath, resolve(dir, node), 'file')
256-
257254
const PATH = process.env.PATH || process.env.Path
258-
process.env.PATH = dir
255+
process.env.PATH = resolve(dir, 'bin')
259256
const { execPath, argv: processArgv } = process
260257
process.argv = [
261258
node,
@@ -294,7 +291,7 @@ t.test('npm.load', t => {
294291
[
295292
'verbose',
296293
'node symlink',
297-
resolve(dir, node),
294+
resolve(dir, 'bin', node),
298295
],
299296
[
300297
'timing',
@@ -303,7 +300,7 @@ t.test('npm.load', t => {
303300
],
304301
])
305302
logs.length = 0
306-
t.equal(process.execPath, resolve(dir, node))
303+
t.equal(process.execPath, resolve(dir, 'bin', node))
307304
})
308305

309306
await npm.commands.ll([], (er) => {

0 commit comments

Comments
 (0)