fix: cli has no write access to the selected global dir (#3206)

close #1203
This commit is contained in:
Zoltan Kochan
2021-02-28 20:19:42 +02:00
committed by GitHub
parent f01366a637
commit aed7124557
26 changed files with 77 additions and 52 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/config": major
---
Remove `pnpm-prefix` setting support.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/config": major
---
`globalDir` is never set. Only the `dir` option is set with the global directory location when the `--global` is used. The pnpm CLI should have access to the global dir, otherwise an exception is thrown.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/plugin-commands-installation": major
---
The --global option should be used when linking from/to the global modules directory.

View File

@@ -37,6 +37,7 @@
"@pnpm/types": "workspace:6.4.0",
"@zkochan/npm-conf": "2.0.2",
"camelcase": "^6.2.0",
"can-write-to-dir": "^1.1.0",
"is-subdir": "^1.1.1",
"ramda": "^0.27.1",
"realpath-missing": "^1.1.0",

View File

@@ -21,10 +21,8 @@ export interface Config {
rawConfig: Record<string, any>, // eslint-disable-line
dryRun?: boolean // This option might be not supported ever
global?: boolean
globalDir: string
dir: string
bin?: string
npmGlobalBinDir: string
bin: string
ignoreScripts?: boolean
save?: boolean
saveProd?: boolean

View File

@@ -5,6 +5,7 @@ export default function findBestGlobalPrefixOnWindows (
defaultNpmGlobalPrefix: string,
env: { [key: string]: string | undefined }
) {
if (process.platform !== 'win32') return defaultNpmGlobalPrefix
if (
(env.LOCALAPPDATA != null && isSubdir(env.LOCALAPPDATA, defaultNpmGlobalPrefix)) ||
(env.APPDATA != null && isSubdir(env.APPDATA, defaultNpmGlobalPrefix))

View File

@@ -5,11 +5,13 @@ import globalBinDir from '@pnpm/global-bin-dir'
import camelcase from 'camelcase'
import loadNpmConf from '@zkochan/npm-conf'
import npmTypes from '@zkochan/npm-conf/lib/types'
import { sync as canWriteToDir } from 'can-write-to-dir'
import os from 'os'
import * as R from 'ramda'
import realpathMissing from 'realpath-missing'
import whichcb from 'which'
import getScopeRegistries, { normalizeRegistry } from './getScopeRegistries'
import findBestGlobalPrefixOnWindows from './findBestGlobalPrefixOnWindows'
import findBestGlobalPrefix from './findBestGlobalPrefix'
import {
Config,
ConfigWithDeprecatedSettings,
@@ -221,16 +223,6 @@ export default async (
default: normalizeRegistry(pnpmConfig.rawConfig.registry),
...getScopeRegistries(pnpmConfig.rawConfig),
}
const npmGlobalPrefix: string = pnpmConfig.globalDir ?? pnpmConfig.rawConfig['pnpm-prefix'] ??
(
process.platform !== 'win32'
? npmConfig.globalPrefix
: findBestGlobalPrefixOnWindows(npmConfig.globalPrefix, process.env)
)
pnpmConfig.npmGlobalBinDir = process.platform === 'win32'
? npmGlobalPrefix
: path.resolve(npmGlobalPrefix, 'bin')
pnpmConfig.globalDir = pnpmConfig.globalDir ? npmGlobalPrefix : path.join(npmGlobalPrefix, 'pnpm-global')
pnpmConfig.lockfileDir = pnpmConfig.lockfileDir ?? pnpmConfig.lockfileDirectory ?? pnpmConfig.shrinkwrapDirectory
pnpmConfig.useLockfile = (() => {
if (typeof pnpmConfig['lockfile'] === 'boolean') return pnpmConfig['lockfile']
@@ -252,14 +244,21 @@ export default async (
: pnpmConfig['sharedWorkspaceLockfile']
if (cliOptions['global']) {
pnpmConfig.save = true
pnpmConfig.dir = path.join(pnpmConfig.globalDir, LAYOUT_VERSION.toString())
const npmGlobalPrefix: string = findBestGlobalPrefix(npmConfig.globalPrefix, process.env)
const globalDirRoot = pnpmConfig['globalDir']
? pnpmConfig['globalDir'] : path.join(firstWithWriteAccess([npmGlobalPrefix, os.homedir()]), 'pnpm-global')
pnpmConfig.dir = path.join(globalDirRoot, LAYOUT_VERSION.toString())
const npmGlobalBinDir = process.platform === 'win32'
? npmGlobalPrefix
: path.resolve(npmGlobalPrefix, 'bin')
pnpmConfig.bin = cliOptions.dir
? (
process.platform === 'win32'
? cliOptions.dir : path.resolve(cliOptions.dir, 'bin')
)
: globalBinDir([pnpmConfig.npmGlobalBinDir], { shouldAllowWrite: opts.globalDirShouldAllowWrite === true })
: globalBinDir([npmGlobalBinDir], { shouldAllowWrite: opts.globalDirShouldAllowWrite === true })
pnpmConfig.save = true
pnpmConfig.allowNew = true
pnpmConfig.ignoreCurrentPrefs = true
pnpmConfig.saveProd = true
@@ -420,3 +419,11 @@ function getProcessEnv (env: string) {
process.env[env.toUpperCase()] ??
process.env[env.toLowerCase()]
}
function firstWithWriteAccess (dirs: string[]) {
const first = dirs.find((dir) => dir.includes('_npx') || canWriteToDir(dir))
if (first == null) {
throw new PnpmError('NO_SUITABLE_GLOBAL_DIR', `pnpm has no write access to global direcotry. Tried locations: ${dirs.join(', ')}`)
}
return first
}

View File

@@ -1,6 +1,6 @@
import findBestGlobalPrefixOnWindows from '../lib/findBestGlobalPrefixOnWindows'
import findBestGlobalPrefix from '../lib/findBestGlobalPrefix'
test('findBestGlobalPrefixOnWindows()', () => {
test('findBestGlobalPrefix()', () => {
if (process.platform !== 'win32') {
// skipping on non-windows
return
@@ -13,24 +13,24 @@ test('findBestGlobalPrefixOnWindows()', () => {
expect(
// keep npm global prefix if is inside AppData\Local
findBestGlobalPrefixOnWindows('C:\\Users\\Imre\\AppData\\Local\\nvs\\default', env)).toEqual(
findBestGlobalPrefix('C:\\Users\\Imre\\AppData\\Local\\nvs\\default', env)).toEqual(
'C:\\Users\\Imre\\AppData\\Local\\nvs\\default'
)
expect(
// keep npm global prefix if is inside AppData\Roaming
findBestGlobalPrefixOnWindows('C:\\Users\\Imre\\AppData\\Roaming\\nvs\\default', env)).toEqual(
findBestGlobalPrefix('C:\\Users\\Imre\\AppData\\Roaming\\nvs\\default', env)).toEqual(
'C:\\Users\\Imre\\AppData\\Roaming\\nvs\\default'
)
expect(
// prefer location in AppData\Roaming
findBestGlobalPrefixOnWindows('C:\\foo', env)).toEqual(
findBestGlobalPrefix('C:\\foo', env)).toEqual(
'C:\\Users\\Imre\\AppData\\Roaming\\npm'
)
expect(
findBestGlobalPrefixOnWindows('C:\\foo', {})).toEqual(
findBestGlobalPrefix('C:\\foo', {})).toEqual(
'C:\\foo'
)
})

View File

@@ -3,7 +3,6 @@ import getConfig from '@pnpm/config'
import PnpmError from '@pnpm/error'
import prepare, { prepareEmpty } from '@pnpm/prepare'
import './findBestGlobalPrefixOnWindows'
import { promises as fs } from 'fs'
import path from 'path'
import symlinkDir from 'symlink-dir'

View File

@@ -31,7 +31,7 @@
"funding": "https://opencollective.com/pnpm",
"dependencies": {
"@pnpm/error": "workspace:1.4.0",
"can-write-to-dir": "^1.0.1",
"can-write-to-dir": "^1.1.0",
"path-name": "^1.0.0"
},
"devDependencies": {

View File

@@ -64,7 +64,6 @@
"@pnpm/filter-workspace-packages": "workspace:2.3.10",
"@pnpm/find-workspace-dir": "workspace:2.0.0",
"@pnpm/find-workspace-packages": "workspace:2.3.38",
"@pnpm/global-bin-dir": "workspace:1.2.6",
"@pnpm/manifest-utils": "workspace:1.1.5",
"@pnpm/outdated": "workspace:7.2.26",
"@pnpm/package-store": "workspace:10.1.18",

View File

@@ -10,7 +10,6 @@ import { UNIVERSAL_OPTIONS } from '@pnpm/common-cli-options-help'
import { Config, types as allTypes } from '@pnpm/config'
import findWorkspaceDir from '@pnpm/find-workspace-dir'
import findWorkspacePackages, { arrayOfWorkspacePackagesToMap } from '@pnpm/find-workspace-packages'
import globalBinDir from '@pnpm/global-bin-dir'
import { StoreController } from '@pnpm/package-store'
import { createOrConnectStoreControllerCached, CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
import {
@@ -74,17 +73,17 @@ export function help () {
export async function handler (
opts: CreateStoreControllerOptions & Pick<Config,
| 'bin'
| 'cliOptions'
| 'engineStrict'
| 'npmGlobalBinDir'
| 'saveDev'
| 'saveOptional'
| 'saveProd'
| 'workspaceDir'
> & Partial<Pick<Config, 'globalDir' | 'linkWorkspacePackages'>>,
> & Partial<Pick<Config, 'linkWorkspacePackages'>>,
params?: string[]
) {
const cwd = opts?.dir ?? process.cwd()
const cwd = process.cwd()
const storeControllerCache = new Map<string, Promise<{dir: string, ctrl: StoreController}>>()
let workspacePackagesArr
@@ -106,12 +105,12 @@ export async function handler (
// pnpm link
if (!params || !params.length) {
const { manifest, writeProjectManifest } = await tryReadProjectManifest(opts.globalDir!, opts)
const { manifest, writeProjectManifest } = await tryReadProjectManifest(opts.dir, opts)
const newManifest = await linkToGlobal(cwd, {
...linkOpts,
// A temporary workaround. global bin/prefix are always defined when --global is set
globalBin: globalBinDir([linkOpts.npmGlobalBinDir]),
globalDir: linkOpts.globalDir!,
dir: cwd,
globalBin: linkOpts.bin,
globalDir: linkOpts.dir,
manifest: manifest ?? {},
})
await writeProjectManifest(newManifest)
@@ -137,7 +136,7 @@ export async function handler (
} else {
globalPkgNames = pkgNames
}
const globalPkgPath = pathAbsolute(opts.globalDir!)
const globalPkgPath = pathAbsolute(opts.dir)
globalPkgNames.forEach((pkgName) => pkgPaths.push(path.join(globalPkgPath, 'node_modules', pkgName)))
}

View File

@@ -13,6 +13,7 @@ const DEFAULT_OPTIONS = {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
cliOptions: {},
include: {
dependencies: true,

View File

@@ -24,12 +24,11 @@ test('linking multiple packages', async () => {
console.log('linking linked-foo to global package')
const linkOpts = {
...DEFAULT_OPTS,
npmGlobalBinDir: path.join(globalDir, 'bin'),
globalDir,
bin: path.join(globalDir, 'bin'),
dir: globalDir,
}
await link.handler({
...linkOpts,
dir: process.cwd(),
})
process.chdir('..')
@@ -37,7 +36,6 @@ test('linking multiple packages', async () => {
await link.handler({
...linkOpts,
dir: process.cwd(),
}, ['linked-foo', '../linked-bar'])
await project.has('linked-foo')
@@ -64,9 +62,8 @@ test('link global bin', async function () {
await link.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
npmGlobalBinDir: globalBin,
globalDir,
bin: globalBin,
dir: globalDir,
})
process.env[PATH] = oldPath
@@ -87,7 +84,6 @@ test('relative link', async () => {
await link.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
npmGlobalBinDir: '',
}, [`../${linkedPkgName}`])
await project.isExecutable('.bin/hello-world-js-bin')
@@ -119,7 +115,6 @@ test('absolute link', async () => {
await link.handler({
...DEFAULT_OPTS,
dir: process.cwd(),
npmGlobalBinDir: '',
}, [linkedPkgPath])
await project.isExecutable('.bin/hello-world-js-bin')
@@ -173,7 +168,6 @@ test('link --production', async () => {
...DEFAULT_OPTS,
cliOptions: { production: true },
dir: process.cwd(),
npmGlobalBinDir: '',
}, ['../source'])
await projects['source'].has('is-positive')

View File

@@ -10,6 +10,7 @@ const DEFAULT_OPTIONS = {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
cliOptions: {},
include: {
dependencies: true,

View File

@@ -11,6 +11,7 @@ const DEFAULT_OPTIONS = {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
cliOptions: {},
include: {
dependencies: true,
@@ -36,7 +37,6 @@ test('prune removes external link that is not in package.json', async () => {
await link.handler({
...DEFAULT_OPTIONS,
dir: process.cwd(),
npmGlobalBinDir: process.cwd(),
storeDir,
}, ['./local'])

View File

@@ -24,6 +24,7 @@ const DEFAULT_OPTIONS = {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
cliOptions: {},
include: {
dependencies: true,

View File

@@ -8,6 +8,7 @@ export const DEFAULT_OPTS = {
original: [],
},
bail: true,
bin: 'node_modules/.bin',
ca: undefined,
cert: undefined,
cliOptions: {},

View File

@@ -45,9 +45,6 @@
{
"path": "../find-workspace-packages"
},
{
"path": "../global-bin-dir"
},
{
"path": "../lockfile-types"
},

View File

@@ -8,6 +8,7 @@ export const DEFAULT_OPTS = {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
ca: undefined,
cert: undefined,
cliOptions: {},

View File

@@ -8,6 +8,7 @@ export const DEFAULT_OPTS = {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
ca: undefined,
cert: undefined,
cliOptions: {},

View File

@@ -8,6 +8,7 @@ export const DEFAULT_OPTS = {
original: [],
},
bail: false,
bin: 'node_modules/.bin',
ca: undefined,
cert: undefined,
cliOptions: {},

View File

@@ -1,6 +1,7 @@
import path from 'path'
import { LAYOUT_VERSION } from '@pnpm/constants'
import prepare from '@pnpm/prepare'
import { promises as fs } from 'fs'
import isWindows from 'is-windows'
import exists from 'path-exists'
import {
@@ -11,6 +12,7 @@ import {
test('global installation', async () => {
prepare()
const global = path.resolve('..', 'global')
await fs.mkdir(global)
const env = { NPM_CONFIG_PREFIX: global }
if (process.env.APPDATA) env['APPDATA'] = global
@@ -44,6 +46,7 @@ test('always install latest when doing global installation without spec', async
await addDistTag('peer-c', '2.0.0', 'latest')
const global = path.resolve('..', 'global')
await fs.mkdir(global)
const env = { NPM_CONFIG_PREFIX: global }
@@ -68,6 +71,7 @@ test('run lifecycle events of global packages in correct working directory', asy
prepare()
const global = path.resolve('..', 'global')
await fs.mkdir(global)
const env = { NPM_CONFIG_PREFIX: global }
if (process.env.APPDATA) env['APPDATA'] = global

View File

@@ -1,5 +1,6 @@
import path from 'path'
import prepare from '@pnpm/prepare'
import { promises as fs } from 'fs'
import isWindows from 'is-windows'
import pathExists from 'path-exists'
import {
@@ -21,6 +22,7 @@ skipOnWindows('self-update stops the store server', async () => {
expect(serverJson.connectionOptions).toBeTruthy()
const global = path.resolve('global')
await fs.mkdir(global)
const env = { NPM_CONFIG_PREFIX: global }
if (process.env.APPDATA) env['APPDATA'] = global

View File

@@ -20,6 +20,7 @@ test('pnpm root -g', async () => {
tempDir()
const global = path.resolve('global')
await fs.mkdir(global)
const env = { NPM_CONFIG_PREFIX: global }
if (process.env.APPDATA) env['APPDATA'] = global

7
pnpm-lock.yaml generated
View File

@@ -240,6 +240,7 @@ importers:
'@pnpm/types': link:../types
'@zkochan/npm-conf': 2.0.2
camelcase: 6.2.0
can-write-to-dir: 1.1.0
is-subdir: 1.2.0
ramda: 0.27.1
realpath-missing: 1.1.0
@@ -261,6 +262,7 @@ importers:
'@types/which': ^2.0.0
'@zkochan/npm-conf': 2.0.2
camelcase: ^6.2.0
can-write-to-dir: ^1.1.0
is-subdir: ^1.1.1
ramda: ^0.27.1
realpath-missing: ^1.1.0
@@ -703,7 +705,7 @@ importers:
specifiers:
'@pnpm/error': workspace:1.4.0
'@pnpm/global-bin-dir': 'link:'
can-write-to-dir: ^1.0.1
can-write-to-dir: ^1.1.0
is-windows: ^1.0.2
path-name: ^1.0.0
@@ -1689,7 +1691,6 @@ importers:
'@pnpm/filter-workspace-packages': link:../filter-workspace-packages
'@pnpm/find-workspace-dir': link:../find-workspace-dir
'@pnpm/find-workspace-packages': link:../find-workspace-packages
'@pnpm/global-bin-dir': link:../global-bin-dir
'@pnpm/manifest-utils': link:../manifest-utils
'@pnpm/outdated': link:../outdated
'@pnpm/package-store': link:../package-store
@@ -1751,7 +1752,6 @@ importers:
'@pnpm/filter-workspace-packages': workspace:2.3.10
'@pnpm/find-workspace-dir': workspace:2.0.0
'@pnpm/find-workspace-packages': workspace:2.3.38
'@pnpm/global-bin-dir': workspace:1.2.6
'@pnpm/lockfile-types': workspace:2.2.0
'@pnpm/logger': ^3.2.3
'@pnpm/manifest-utils': workspace:1.1.5
@@ -6271,6 +6271,7 @@ packages:
resolution: {integrity: sha512-PYRx9toaBwM5kk+GTFuijayF5omWa/ToRCSlMUe2YA2Ki8+j0vOc+Hoe1bf7C5l1lQA2y3tE2t+PeQ4XiTlibg==}
dependencies:
path-temp: 2.0.0
dev: true
engines:
node: '>=10.13'