Skip to content

Commit

Permalink
fix(config): checking global-bin-dir in PATH (#4751)
Browse files Browse the repository at this point in the history
When global-bin-dir is pointing to a symlink.

close #4744
  • Loading branch information
zkochan authored May 17, 2022
1 parent e2ac82f commit af22c6c
Showing 4 changed files with 62 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changeset/chilled-taxis-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/config": patch
"pnpm": patch
---

When the global bin directory is set to a symlink, check not only the symlink in the PATH but also the target of the symlink [#4744](https://github.com/pnpm/pnpm/issues/4744).
15 changes: 11 additions & 4 deletions packages/config/src/checkGlobalBinDir.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import { promises as fs } from 'fs'
import path from 'path'
import PnpmError from '@pnpm/error'
import { sync as canWriteToDir } from 'can-write-to-dir'
import PATH from 'path-name'

export function checkGlobalBinDir (
export async function checkGlobalBinDir (
globalBinDir: string,
{ env, shouldAllowWrite }: { env: Record<string, string | undefined>, shouldAllowWrite?: boolean }
): void {
): Promise<void> {
if (!env[PATH]) {
throw new PnpmError('NO_PATH_ENV',
`Couldn't find a global directory for executables because the "${PATH}" environment variable is not set.`)
}
const dirs = env[PATH]?.split(path.delimiter) ?? []
if (!dirs.some((dir) => areDirsEqual(globalBinDir, dir))) {
if (!await globalBinDirIsInPath(globalBinDir, env)) {
throw new PnpmError('GLOBAL_BIN_DIR_NOT_IN_PATH', `The configured global bin directory "${globalBinDir}" is not in PATH`)
}
if (shouldAllowWrite && !canWriteToDirAndExists(globalBinDir)) {
throw new PnpmError('PNPM_DIR_NOT_WRITABLE', `The CLI has no write access to the pnpm home directory at ${globalBinDir}`)
}
}

async function globalBinDirIsInPath (globalBinDir: string, env: Record<string, string | undefined>) {
const dirs = env[PATH]?.split(path.delimiter) ?? []
if (dirs.some((dir) => areDirsEqual(globalBinDir, dir))) return true
const realGlobalBinDir = await fs.realpath(globalBinDir)
return dirs.some((dir) => areDirsEqual(realGlobalBinDir, dir))
}

const areDirsEqual = (dir1: string, dir2: string) =>
path.relative(dir1, dir2) === ''

2 changes: 1 addition & 1 deletion packages/config/src/index.ts
Original file line number Diff line number Diff line change
@@ -275,7 +275,7 @@ export default async (
pnpmConfig.bin = npmConfig.get('global-bin-dir') ?? env.PNPM_HOME
if (pnpmConfig.bin) {
fs.mkdirSync(pnpmConfig.bin, { recursive: true })
checkGlobalBinDir(pnpmConfig.bin, { env, shouldAllowWrite: opts.globalDirShouldAllowWrite })
await checkGlobalBinDir(pnpmConfig.bin, { env, shouldAllowWrite: opts.globalDirShouldAllowWrite })
} else {
throw new PnpmError('NO_GLOBAL_BIN_DIR', 'Unable to find the global bin directory', {
hint: 'Run "pnpm setup" to create it automatically, or set the global-bin-dir setting, or the PNPM_HOME env variable. The global bin directory should be in the PATH.',
44 changes: 44 additions & 0 deletions packages/config/test/globalBinDir.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/// <reference path="../../../typings/index.d.ts"/>
import fs from 'fs'
import { tempDir } from '@pnpm/prepare'
import path from 'path'
import pathName from 'path-name'
import symlinkDir from 'symlink-dir'
import { homedir } from 'os'
import getConfig from '@pnpm/config'

@@ -63,3 +66,44 @@ test('respects global-bin-dir rather than dir', async () => {
})
expect(config.bin).toBe(globalBinDir)
})

test('an exception is thrown when the global dir is not in PATH', async () => {
await expect(
getConfig({
cliOptions: {
global: true,
dir: __dirname,
},
env: {
[pathName]: process.env[pathName],
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
).rejects.toThrow(/is not in PATH/)
})

test('the global directory may be a symlink to a directory that is in PATH', async () => {
const tmp = tempDir()
const globalBinDirTarget = path.join(tmp, 'global-target')
fs.mkdirSync(globalBinDirTarget)
const globalBinDirSymlink = path.join(tmp, 'global-symlink')
await symlinkDir(globalBinDirTarget, globalBinDirSymlink)
const { config } = await getConfig({
cliOptions: {
global: true,
'global-bin-dir': globalBinDirSymlink,
dir: __dirname,
},
env: {
[pathName]: `${globalBinDirTarget}${path.delimiter}${process.env[pathName]!}`,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})
expect(config.bin).toBe(globalBinDirSymlink)
})

0 comments on commit af22c6c

Please sign in to comment.