fix: the lockfile should be autofixed if it has invalid checksums (#3228)

close #3137
This commit is contained in:
Zoltan Kochan
2021-03-09 05:30:27 +02:00
committed by Zoltan Kochan
parent 8d1dfa89c6
commit f008425cdb
7 changed files with 102 additions and 45 deletions

View File

@@ -0,0 +1,5 @@
---
"supi": patch
---
Fix the lockfile if it contains invalid checksums.

View File

@@ -66,6 +66,11 @@ import pFilter = require('p-filter')
import pLimit = require('p-limit')
import R = require('ramda')
const BROKEN_LOCKFILE_INTEGRITY_ERRORS = new Set([
'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE',
'ERR_PNPM_TARBALL_INTEGRITY',
])
export type DependenciesMutation = (
{
buildIndex: number
@@ -257,9 +262,9 @@ export async function mutateModules (
} catch (error) {
if (
frozenLockfile ||
error.code !== 'ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY' && error.code !== 'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE'
error.code !== 'ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY' && !BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code)
) throw error
if (error.code === 'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE') {
if (BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code)) {
needsFullResolution = true
// Ideally, we would not update but currently there is no other way to redownload the integrity of the package
opts.update = true
@@ -267,9 +272,10 @@ export async function mutateModules (
// A broken lockfile may be caused by a badly resolved Git conflict
logger.warn({
error,
message: 'The lockfile is broken! Resolution step will be performed to fix it.',
message: error.message,
prefix: ctx.lockfileDir,
})
logger.error(new PnpmError(error.code, 'The lockfile is broken! Resolution step will be performed to fix it.'))
}
}
}
@@ -897,17 +903,16 @@ const installInContext: InstallFunction = async (projects, ctx, opts) => {
try {
return await _installInContext(projects, ctx, opts)
} catch (error) {
if (
error.code !== 'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE'
) throw error
if (!BROKEN_LOCKFILE_INTEGRITY_ERRORS.has(error.code)) throw error
opts.needsFullResolution = true
// Ideally, we would not update but currently there is no other way to redownload the integrity of the package
opts.update = true
logger.warn({
error,
message: 'The lockfile is broken! pnpm will attempt to fix it.',
message: error.message,
prefix: ctx.lockfileDir,
})
logger.error(new PnpmError(error.code, 'The lockfile is broken! A full installation will be performed in an attempt to fix it.'))
return _installInContext(projects, ctx, opts)
}
}

View File

@@ -1,6 +1,7 @@
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { prepareEmpty } from '@pnpm/prepare'
import rimraf from '@zkochan/rimraf'
import R from 'ramda'
import {
addDependenciesToPackage,
mutateModules,
@@ -22,10 +23,11 @@ test('installation breaks if the lockfile contains the wrong checksum', async ()
await testDefaults({ lockfileOnly: true })
)
const lockfile = await project.readLockfile()
const corruptedLockfile = await project.readLockfile()
const correctLockfile = R.clone(corruptedLockfile)
// breaking the lockfile
lockfile.packages['/pkg-with-1-dep/100.0.0'].resolution['integrity'] = lockfile.packages['/dep-of-pkg-with-1-dep/100.0.0'].resolution['integrity']
await writeYamlFile(WANTED_LOCKFILE, lockfile, { lineWidth: 1000 })
corruptedLockfile.packages['/pkg-with-1-dep/100.0.0'].resolution['integrity'] = corruptedLockfile.packages['/dep-of-pkg-with-1-dep/100.0.0'].resolution['integrity']
await writeYamlFile(WANTED_LOCKFILE, corruptedLockfile, { lineWidth: 1000 })
await expect(mutateModules([
{
@@ -45,8 +47,10 @@ test('installation breaks if the lockfile contains the wrong checksum', async ()
},
], await testDefaults())
expect(await project.readLockfile()).toStrictEqual(correctLockfile)
// Breaking the lockfile again
await writeYamlFile(WANTED_LOCKFILE, lockfile, { lineWidth: 1000 })
await writeYamlFile(WANTED_LOCKFILE, corruptedLockfile, { lineWidth: 1000 })
await rimraf('node_modules')
@@ -58,4 +62,76 @@ test('installation breaks if the lockfile contains the wrong checksum', async ()
rootDir: process.cwd(),
},
], await testDefaults({ preferFrozenLockfile: false }))
expect(await project.readLockfile()).toStrictEqual(correctLockfile)
})
test('installation breaks if the lockfile contains the wrong checksum and the store is clean', async () => {
await addDistTag('dep-of-pkg-with-1-dep', '100.0.0', 'latest')
const project = prepareEmpty()
const manifest = await addDependenciesToPackage({},
[
'pkg-with-1-dep@100.0.0',
],
await testDefaults({ lockfileOnly: true })
)
const corruptedLockfile = await project.readLockfile()
const correctIntegrity = corruptedLockfile.packages['/pkg-with-1-dep/100.0.0'].resolution['integrity']
// breaking the lockfile
corruptedLockfile.packages['/pkg-with-1-dep/100.0.0'].resolution['integrity'] = 'sha512-pl8WtlGAnoIQ7gPxT187/YwhKRnsFBR4h0YY+v0FPQjT5WPuZbI9dPRaKWgKBFOqWHylJ8EyPy34V5u9YArfng=='
await writeYamlFile(WANTED_LOCKFILE, corruptedLockfile, { lineWidth: 1000 })
await expect(
mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
],
await testDefaults({ frozenLockfile: true }, { retry: { retries: 0 } }))
).rejects.toThrowError(/Got unexpected checksum/)
await mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
], await testDefaults({}, { retry: { retries: 0 } }))
{
const lockfile = await project.readLockfile()
expect(lockfile.packages['/pkg-with-1-dep/100.0.0'].resolution['integrity']).toBe(correctIntegrity)
}
// Breaking the lockfile again
await writeYamlFile(WANTED_LOCKFILE, corruptedLockfile, { lineWidth: 1000 })
await rimraf('node_modules')
const reporter = jest.fn()
await mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
], await testDefaults({ preferFrozenLockfile: false, reporter }, { retry: { retries: 0 } }))
expect(reporter).toBeCalledWith(expect.objectContaining({
level: 'warn',
name: 'pnpm',
prefix: process.cwd(),
message: expect.stringMatching(/Got unexpected checksum/),
}))
{
const lockfile = await project.readLockfile()
expect(lockfile.packages['/pkg-with-1-dep/100.0.0'].resolution['integrity']).toBe(correctIntegrity)
}
})

View File

@@ -116,39 +116,6 @@ test('lockfile with scoped package', async () => {
}, await testDefaults({ frozenLockfile: true }))
})
test('fail when shasum from lockfile does not match with the actual one', async () => {
prepareEmpty()
await writeYamlFile(WANTED_LOCKFILE, {
dependencies: {
'is-negative': '2.1.0',
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'/is-negative/2.1.0': {
resolution: {
integrity: 'sha1-uZnX2TX0P1IHsBsA094ghS9Mp10=',
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-negative/-/is-negative-2.1.0.tgz`,
},
},
},
specifiers: {
'is-negative': '2.1.0',
},
})
try {
await install({
dependencies: {
'is-negative': '2.1.0',
},
}, await testDefaults({}, {}, { fetchRetries: 0 }))
throw new Error('installation should have failed')
} catch (err) {
expect(err.code).toBe('ERR_PNPM_TARBALL_INTEGRITY')
}
})
test("lockfile doesn't lock subdependencies that don't satisfy the new specs", async () => {
const project = prepareEmpty()

2
pnpm-lock.yaml generated
View File

@@ -3190,6 +3190,7 @@ importers:
dependencies:
'@pnpm/assert-project': link:../assert-project
'@pnpm/types': link:../../packages/types
unique-string: 2.0.0
write-json5-file: 3.1.0
write-pkg: 4.0.0
write-yaml-file: 4.2.0
@@ -3203,6 +3204,7 @@ importers:
'@types/node': ^14.14.22
tslint-config-standard: 9.0.0
tslint-eslint-rules: 5.4.0
unique-string: ^2.0.0
write-json5-file: ^3.0.1
write-pkg: 4.0.0
write-yaml-file: ^4.1.3

View File

@@ -7,6 +7,7 @@
"dependencies": {
"@pnpm/assert-project": "workspace:*",
"@pnpm/types": "workspace:6.4.0",
"unique-string": "^2.0.0",
"write-json5-file": "^3.0.1",
"write-pkg": "4.0.0",
"write-yaml-file": "^4.1.3"

View File

@@ -1,5 +1,6 @@
import assertProject, { Modules, Project } from '@pnpm/assert-project'
import { ProjectManifest } from '@pnpm/types'
import uniqueString from 'unique-string'
import { sync as writeJson5File } from 'write-json5-file'
import { sync as writeYamlFile } from 'write-yaml-file'
import fs = require('fs')
@@ -11,7 +12,7 @@ export type ManifestFormat = 'JSON' | 'JSON5' | 'YAML'
// The testing folder should be outside of the project to avoid lookup in the project's node_modules
// Not using the OS temp directory due to issues on Windows CI.
const tmpPath = path.join(__dirname, `../../../../pnpm_tmp/${Math.random()}`)
const tmpPath = path.join(__dirname, `../../../../pnpm_tmp/${uniqueString()}`)
let dirNumber = 0