fix: cross-device link not permitted when node-linker=hoisted (#5993)

close #5992
This commit is contained in:
Zoltan Kochan
2023-01-29 04:47:06 +02:00
committed by GitHub
parent 2ff11e77a6
commit 78d4cf1f73
9 changed files with 148 additions and 30 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/fs.hard-link-dir": patch
---
Fall back to copying files if creating hard links fails with cross-device linking error [#5992](https://github.com/pnpm/pnpm/issues/5992).

View File

@@ -0,0 +1,6 @@
---
"@pnpm/fs.indexed-pkg-importer": patch
"pnpm": patch
---
Fix "cross-device link not permitted" error when `node-linker` is set to `hoisted` [#5992](https://github.com/pnpm/pnpm/issues/5992).

View File

@@ -1,6 +1,6 @@
# @pnpm/fs.hard-link-dir
> Hard link all files from a directory to several target directories.
> Hard link (or copy if linking fails) all files from a directory to several target directories.
<!--@shields('npm')-->
[![npm version](https://img.shields.io/npm/v/hard-link-dir.svg)](https://www.npmjs.com/package/@pnpm/fs.hard-link-dir)
@@ -14,4 +14,4 @@ pnpm add @pnpm/fs.hard-link-dir
## License
MIT © [Zoltan Kochan](https://www.kochan.io)
MIT

View File

@@ -1,7 +1,7 @@
{
"name": "@pnpm/fs.hard-link-dir",
"version": "1.0.1",
"description": "Hard link all files from a directory to several target directories.",
"description": "Hard link (or copy if linking fails) all files from a directory to several target directories.",
"main": "lib/index.js",
"files": [
"lib",

View File

@@ -28,11 +28,11 @@ export async function hardLinkDir (src: string, destDirs: string[]) {
destDirs.map(async (destDir) => {
const destFile = path.join(destDir, file)
try {
await fs.link(srcFile, destFile)
await linkOrCopy(srcFile, destFile)
} catch (err: any) { // eslint-disable-line
if (err.code === 'ENOENT') {
await fs.mkdir(destDir, { recursive: true })
await fs.link(srcFile, destFile)
await linkOrCopy(srcFile, destFile)
return
}
if (err.code !== 'EEXIST') {
@@ -44,3 +44,16 @@ export async function hardLinkDir (src: string, destDirs: string[]) {
})
)
}
/*
* This function could be optimized because we don't really need to try linking again
* if linking failed once.
*/
async function linkOrCopy (srcFile: string, destFile: string) {
try {
await fs.link(srcFile, destFile)
} catch (err: any) { // eslint-disable-line
if (err.code !== 'EXDEV') throw err
await fs.copyFile(srcFile, destFile)
}
}

View File

@@ -18,6 +18,7 @@
"@pnpm/core-loggers": "workspace:*",
"@pnpm/store-controller-types": "workspace:*",
"@zkochan/rimraf": "^2.1.2",
"fs-extra": "^11.1.0",
"make-empty-dir": "^2.0.0",
"p-limit": "^3.1.0",
"path-exists": "^4.0.0",
@@ -27,7 +28,8 @@
},
"devDependencies": {
"@pnpm/fs.indexed-pkg-importer": "workspace:*",
"@pnpm/prepare": "workspace:*"
"@pnpm/prepare": "workspace:*",
"@types/fs-extra": "^9.0.13"
},
"directories": {
"test": "test"

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'
import { copy } from 'fs-extra'
import path from 'path'
import { globalWarn, logger } from '@pnpm/logger'
import rimraf from '@zkochan/rimraf'
@@ -118,7 +119,7 @@ function getUniqueFileMap (fileMap: Record<string, string>) {
async function moveOrMergeModulesDirs (src: string, dest: string) {
try {
await fs.rename(src, dest)
await renameEvenAcrossDevices(src, dest)
} catch (err: any) { // eslint-disable-line
switch (err.code) {
case 'ENOENT':
@@ -135,9 +136,18 @@ async function moveOrMergeModulesDirs (src: string, dest: string) {
}
}
async function renameEvenAcrossDevices (src: string, dest: string) {
try {
await fs.rename(src, dest)
} catch (err: any) { // eslint-disable-line
if (err.code !== 'EXDEV') throw err
await copy(src, dest)
}
}
async function mergeModulesDirs (src: string, dest: string) {
const srcFiles = await fs.readdir(src)
const destFiles = new Set(await fs.readdir(dest))
const filesToMove = srcFiles.filter((file) => !destFiles.has(file))
await Promise.all(filesToMove.map((file) => fs.rename(path.join(src, file), path.join(dest, file))))
await Promise.all(filesToMove.map((file) => renameEvenAcrossDevices(path.join(src, file), path.join(dest, file))))
}

View File

@@ -11,6 +11,9 @@ jest.mock('fs', () => {
})
jest.mock('path-temp', () => (dir: string) => path.join(dir, '_tmp'))
jest.mock('rename-overwrite', () => jest.fn())
jest.mock('fs-extra', () => ({
copy: jest.fn(),
}))
const globalInfo = jest.fn()
const globalWarn = jest.fn()
const logger = jest.fn(() => ({ debug: jest.fn() }))

123
pnpm-lock.yaml generated
View File

@@ -1500,6 +1500,9 @@ importers:
'@zkochan/rimraf':
specifier: ^2.1.2
version: 2.1.2
fs-extra:
specifier: ^11.1.0
version: 11.1.0
make-empty-dir:
specifier: ^2.0.0
version: 2.0.0
@@ -1525,6 +1528,9 @@ importers:
'@pnpm/prepare':
specifier: workspace:*
version: link:../../__utils__/prepare
'@types/fs-extra':
specifier: ^9.0.13
version: 9.0.13
fs/read-modules-dir:
dependencies:
@@ -6598,15 +6604,15 @@ packages:
'@commitlint/execute-rule': 17.4.0
'@commitlint/resolve-extends': 17.4.0
'@commitlint/types': 17.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
chalk: 4.1.2
cosmiconfig: 8.0.0
cosmiconfig-typescript-loader: 4.3.0(@types/node@14.18.36)(cosmiconfig@8.0.0)(ts-node@10.9.1)(typescript@4.9.4)
cosmiconfig-typescript-loader: 4.3.0(@types/node@18.11.18)(cosmiconfig@8.0.0)(ts-node@10.9.1)(typescript@4.9.4)
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
lodash.uniq: 4.5.0
resolve-from: 5.0.0
ts-node: 10.9.1(@types/node@14.18.36)(typescript@4.9.4)
ts-node: 10.9.1(@types/node@18.11.18)(typescript@4.9.4)
typescript: 4.9.4
transitivePeerDependencies:
- '@swc/core'
@@ -6973,7 +6979,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
chalk: 4.1.2
jest-message-util: 29.4.0
jest-util: 29.4.0
@@ -6994,14 +7000,14 @@ packages:
'@jest/test-result': 29.4.0
'@jest/transform': 29.4.0(@babel/types@7.20.7)
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.7.1
exit: 0.1.2
graceful-fs: 4.2.10
jest-changed-files: 29.4.0
jest-config: 29.4.0(@babel/types@7.20.7)(@types/node@14.18.36)(ts-node@10.9.1)
jest-config: 29.4.0(@babel/types@7.20.7)(@types/node@18.11.18)(ts-node@10.9.1)
jest-haste-map: 29.4.0
jest-message-util: 29.4.0
jest-regex-util: 29.2.0
@@ -7029,7 +7035,7 @@ packages:
dependencies:
'@jest/fake-timers': 29.4.0
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
jest-mock: 29.4.0
dev: true
@@ -7056,7 +7062,7 @@ packages:
dependencies:
'@jest/types': 29.4.0
'@sinonjs/fake-timers': 10.0.2
'@types/node': 14.18.36
'@types/node': 18.11.18
jest-message-util: 29.4.0
jest-mock: 29.4.0
jest-util: 29.4.0
@@ -7089,7 +7095,7 @@ packages:
'@jest/transform': 29.4.0(@babel/types@7.20.7)
'@jest/types': 29.4.0
'@jridgewell/trace-mapping': 0.3.17
'@types/node': 14.18.36
'@types/node': 18.11.18
chalk: 4.1.2
collect-v8-coverage: 1.0.1
exit: 0.1.2
@@ -7179,7 +7185,7 @@ packages:
'@jest/schemas': 29.4.0
'@types/istanbul-lib-coverage': 2.0.4
'@types/istanbul-reports': 3.0.1
'@types/node': 14.18.36
'@types/node': 18.11.18
'@types/yargs': 17.0.20
chalk: 4.1.2
dev: true
@@ -10501,7 +10507,7 @@ packages:
object-assign: 4.1.1
vary: 1.1.2
/cosmiconfig-typescript-loader@4.3.0(@types/node@14.18.36)(cosmiconfig@8.0.0)(ts-node@10.9.1)(typescript@4.9.4):
/cosmiconfig-typescript-loader@4.3.0(@types/node@18.11.18)(cosmiconfig@8.0.0)(ts-node@10.9.1)(typescript@4.9.4):
resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==}
engines: {node: '>=12', npm: '>=6'}
peerDependencies:
@@ -10510,9 +10516,9 @@ packages:
ts-node: '>=10'
typescript: '>=3'
dependencies:
'@types/node': 14.18.36
'@types/node': 18.11.18
cosmiconfig: 8.0.0
ts-node: 10.9.1(@types/node@14.18.36)(typescript@4.9.4)
ts-node: 10.9.1(@types/node@18.11.18)(typescript@4.9.4)
typescript: 4.9.4
dev: true
@@ -12843,7 +12849,7 @@ packages:
'@jest/expect': 29.4.0
'@jest/test-result': 29.4.0
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
chalk: 4.1.2
co: 4.6.0
dedent: 0.7.0
@@ -12933,6 +12939,47 @@ packages:
- supports-color
dev: true
/jest-config@29.4.0(@babel/types@7.20.7)(@types/node@18.11.18)(ts-node@10.9.1):
resolution: {integrity: sha512-jtgd72nN4Mob4Oego3N/pLRVfR2ui1hv+yO6xR/SUi5G7NtZ/grr95BJ1qRSDYZshuA0Jw57fnttZHZKb04+CA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@types/node': '*'
ts-node: '>=9.0.0'
peerDependenciesMeta:
'@types/node':
optional: true
ts-node:
optional: true
dependencies:
'@babel/core': 7.20.12
'@jest/test-sequencer': 29.4.0
'@jest/types': 29.4.0
'@types/node': 18.11.18
babel-jest: 29.4.0(@babel/core@7.20.12)(@babel/types@7.20.7)
chalk: 4.1.2
ci-info: 3.7.1
deepmerge: 4.2.2
glob: 7.2.3
graceful-fs: 4.2.10
jest-circus: 29.4.0(@babel/types@7.20.7)
jest-environment-node: 29.4.0
jest-get-type: 29.2.0
jest-regex-util: 29.2.0
jest-resolve: 29.4.0
jest-runner: 29.4.0(@babel/types@7.20.7)
jest-util: 29.4.0
jest-validate: 29.4.0
micromatch: 4.0.5
parse-json: 5.2.0
pretty-format: 29.4.0
slash: 3.0.0
strip-json-comments: 3.1.1
ts-node: 10.9.1(@types/node@14.18.36)(typescript@4.9.4)
transitivePeerDependencies:
- '@babel/types'
- supports-color
dev: true
/jest-diff@29.3.1:
resolution: {integrity: sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -12978,7 +13025,7 @@ packages:
'@jest/environment': 29.4.0
'@jest/fake-timers': 29.4.0
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
jest-mock: 29.4.0
jest-util: 29.4.0
dev: true
@@ -12994,7 +13041,7 @@ packages:
dependencies:
'@jest/types': 29.4.0
'@types/graceful-fs': 4.1.6
'@types/node': 14.18.36
'@types/node': 18.11.18
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.10
@@ -13045,7 +13092,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
jest-util: 29.4.0
dev: true
@@ -13100,7 +13147,7 @@ packages:
'@jest/test-result': 29.4.0
'@jest/transform': 29.4.0(@babel/types@7.20.7)
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.10
@@ -13132,7 +13179,7 @@ packages:
'@jest/test-result': 29.4.0
'@jest/transform': 29.4.0(@babel/types@7.20.7)
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
chalk: 4.1.2
cjs-module-lexer: 1.2.2
collect-v8-coverage: 1.0.1
@@ -13190,7 +13237,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
chalk: 4.1.2
ci-info: 3.7.1
graceful-fs: 4.2.10
@@ -13215,7 +13262,7 @@ packages:
dependencies:
'@jest/test-result': 29.4.0
'@jest/types': 29.4.0
'@types/node': 14.18.36
'@types/node': 18.11.18
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@@ -13227,7 +13274,7 @@ packages:
resolution: {integrity: sha512-dICMQ+Q4W0QVMsaQzWlA1FVQhKNz7QcDCOGtbk1GCAd0Lai+wdkQvfmQwL4MjGumineh1xz+6M5oMj3rfWS02A==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@types/node': 14.18.36
'@types/node': 18.11.18
jest-util: 29.4.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -14438,6 +14485,7 @@ packages:
/npmlog@4.1.2:
resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==}
requiresBuild: true
dependencies:
are-we-there-yet: 1.1.7
console-control-strings: 1.1.0
@@ -16573,6 +16621,37 @@ packages:
yn: 3.1.1
dev: true
/ts-node@10.9.1(@types/node@18.11.18)(typescript@4.9.4):
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.3
'@types/node': 18.11.18
acorn: 8.8.2
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.9.4
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/ts-toolbelt@6.15.5:
resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==}
dev: true