Skip to content

Commit ddff4c5

Browse files
authored
Refactored integrity generation and check (#2818)
* proper integrity check: first commit * moved the rest code to integrity-check and integrated with install and check commands * more fixes * removed todo * removed hashes from integrity error message * typos fixes * replaced todo with a proper comment * added a test for bailout * changed integrity hash location logic a bit
1 parent d093c6c commit ddff4c5

File tree

7 files changed

+212
-186
lines changed

7 files changed

+212
-186
lines changed

__tests__/commands/install/integration.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,27 @@ test.concurrent('install should write and read integrity file based on lockfile
748748
});
749749
});
750750

751+
test.concurrent('install should not continue if integrity check passes', (): Promise<void> => {
752+
return runInstall({}, 'lockfile-stability', async (config, reporter) => {
753+
754+
await fs.writeFile(path.join(config.cwd, 'node_modules', 'yarn.test'), 'YARN TEST');
755+
756+
// install should bail out with integrity check and not remove extraneous file
757+
let reinstall = new Install({}, config, reporter, await Lockfile.fromDirectory(config.cwd));
758+
await reinstall.init();
759+
760+
expect(await fs.exists(path.join(config.cwd, 'node_modules', 'yarn.test')));
761+
762+
await fs.unlink(path.join(config.cwd, 'node_modules', 'yarn.test'));
763+
764+
reinstall = new Install({}, config, reporter, await Lockfile.fromDirectory(config.cwd));
765+
await reinstall.init();
766+
767+
expect(!await fs.exists(path.join(config.cwd, 'node_modules', 'yarn.test')));
768+
769+
});
770+
});
771+
751772
test.concurrent('install should not rewrite lockfile with no substantial changes', (): Promise<void> => {
752773
const fixture = 'lockfile-no-rewrites';
753774

__tests__/commands/install/unit.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,6 @@ const path = require('path');
1010

1111
const fixturesLoc = path.join(__dirname, '..', '..', 'fixtures', 'install');
1212

13-
test.concurrent('integrity hash respects flat and production flags', async () => {
14-
const cwd = path.join(fixturesLoc, 'noop');
15-
const reporter = new NoopReporter();
16-
const config = await Config.create({cwd}, reporter);
17-
18-
const lockfile = new Lockfile();
19-
20-
const install = new Install({}, config, reporter, lockfile);
21-
22-
const install2 = new Install({flat: true}, config, reporter, lockfile);
23-
expect(install2.generateIntegrityHash('foo', [])).not.toEqual(install.generateIntegrityHash('foo', []));
24-
25-
const config2 = await Config.create({cwd, production: true}, reporter);
26-
const install3 = new Install({}, config2, reporter, lockfile);
27-
expect(install3.generateIntegrityHash('foo', [])).not.toEqual(install.generateIntegrityHash('foo', []));
28-
expect(install3.generateIntegrityHash('foo', [])).not.toEqual(install2.generateIntegrityHash('foo', []));
29-
});
30-
3113
test.concurrent('flat arg is inherited from root manifest', async (): Promise<void> => {
3214
const cwd = path.join(fixturesLoc, 'top-level-flat-parameter');
3315
const reporter = new NoopReporter();

src/cli/commands/check.js

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/* @flow */
22

3-
import type {Reporter} from '../../reporters/index.js';
43
import type Config from '../../config.js';
54
import {MessageError} from '../../errors.js';
6-
import {Install} from './install.js';
5+
import InstallationIntegrityChecker from '../../integrity-checker.js';
6+
import lockStringify from '../../lockfile/stringify.js';
77
import Lockfile from '../../lockfile/wrapper.js';
8+
import type {Reporter} from '../../reporters/index.js';
89
import * as fs from '../../util/fs.js';
10+
import {Install} from './install.js';
911

1012
const semver = require('semver');
1113
const path = require('path');
@@ -142,29 +144,26 @@ async function integrityHashCheck(
142144
reporter.error(reporter.lang(msg, ...vars));
143145
errCount++;
144146
}
147+
const integrityChecker = new InstallationIntegrityChecker(config);
145148

146149
const lockfile = await Lockfile.fromDirectory(config.cwd);
147150
const install = new Install(flags, config, reporter, lockfile);
148151

149152
// get patterns that are installed when running `yarn install`
153+
// TODO hydrate takes longer then install command bailout https://github.com/yarnpkg/yarn/issues/2514
150154
const {patterns: rawPatterns} = await install.hydrate(true);
151155
const patterns = await install.flatten(rawPatterns);
156+
const lockSource = lockStringify(lockfile.getLockfile(install.resolver.patterns));
152157

153-
// check if patterns exist in lockfile
154-
for (const pattern of patterns) {
155-
if (!lockfile.getLocked(pattern)) {
156-
reportError('lockfileNotContainPatter', pattern);
157-
}
158+
const match = await integrityChecker.check(patterns, lockfile, lockSource, flags);
159+
for (const pattern of match.missingPatterns) {
160+
reportError('lockfileNotContainPattern', pattern);
158161
}
159-
160-
const integrityLoc = await install.getIntegrityHashLocation();
161-
if (integrityLoc && await fs.exists(integrityLoc)) {
162-
const match = await install.matchesIntegrityHash(patterns);
163-
if (match.matches === false) {
164-
reportError('integrityHashesDontMatch', match.expected, match.actual);
165-
}
166-
} else {
167-
reportError('noIntegirtyHashFile');
162+
if (match.integrityFileMissing) {
163+
reportError('noIntegrityHashFile');
164+
}
165+
if (!match.integrityHashMatches) {
166+
reportError('integrityHashesDontMatch');
168167
}
169168

170169
if (errCount > 0) {
@@ -219,7 +218,7 @@ export async function run(
219218
// check if patterns exist in lockfile
220219
for (const pattern of patterns) {
221220
if (!lockfile.getLocked(pattern)) {
222-
reportError('lockfileNotContainPatter', pattern);
221+
reportError('lockfileNotContainPattern', pattern);
223222
}
224223
}
225224

src/cli/commands/install.js

Lines changed: 20 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import type {Manifest, DependencyRequestPatterns} from '../../types.js';
66
import type Config from '../../config.js';
77
import type {RegistryNames} from '../../registries/index.js';
88
import normalizeManifest from '../../util/normalize-manifest/index.js';
9-
import {registryNames} from '../../registries/index.js';
109
import {MessageError} from '../../errors.js';
10+
import InstallationIntegrityChecker from '../../integrity-checker.js';
1111
import Lockfile from '../../lockfile/wrapper.js';
1212
import lockStringify from '../../lockfile/stringify.js';
1313
import PackageFetcher from '../../package-fetcher.js';
@@ -20,9 +20,7 @@ import {registries} from '../../registries/index.js';
2020
import {clean} from './clean.js';
2121
import * as constants from '../../constants.js';
2222
import * as fs from '../../util/fs.js';
23-
import * as crypto from '../../util/crypto.js';
2423
import map from '../../util/map.js';
25-
import {sortAlpha} from '../../util/misc.js';
2624

2725
const invariant = require('invariant');
2826
const semver = require('semver');
@@ -41,13 +39,6 @@ export type InstallCwdRequest = {
4139
manifest: Object,
4240
};
4341

44-
export type IntegrityMatch = {
45-
actual: string,
46-
expected: string,
47-
loc: string,
48-
matches: boolean,
49-
};
50-
5142
type Flags = {
5243
// install
5344
har: boolean,
@@ -179,6 +170,7 @@ export class Install {
179170

180171
this.resolver = new PackageResolver(config, lockfile);
181172
this.fetcher = new PackageFetcher(config, this.resolver);
173+
this.integrityChecker = new InstallationIntegrityChecker(config);
182174
this.compatibility = new PackageCompatibility(config, this.resolver, this.flags.ignoreEngines);
183175
this.linker = new PackageLinker(config, this.resolver);
184176
this.scripts = new PackageInstallScripts(config, this.resolver, this.flags.force);
@@ -197,6 +189,7 @@ export class Install {
197189
compatibility: PackageCompatibility;
198190
fetcher: PackageFetcher;
199191
rootPatternsToOrigin: { [pattern: string]: string };
192+
integrityChecker: InstallationIntegrityChecker;
200193

201194
/**
202195
* Create a list of dependency requests from the current directories manifests.
@@ -301,22 +294,23 @@ export class Install {
301294
async bailout(
302295
patterns: Array<string>,
303296
): Promise<boolean> {
304-
if (this.flags.frozenLockfile && !this.lockFileInSync(patterns)) {
305-
throw new MessageError(this.reporter.lang('frozenLockfileError'));
306-
}
307297
if (this.flags.skipIntegrityCheck || this.flags.force) {
308298
return false;
309299
}
300+
const lockSource = lockStringify(this.lockfile.getLockfile(this.resolver.patterns));
301+
const match = await this.integrityChecker.check(patterns, this.lockfile, lockSource, this.flags);
302+
if (this.flags.frozenLockfile && match.missingPatterns.length > 0) {
303+
throw new MessageError(this.reporter.lang('frozenLockfileError'));
304+
}
310305

311-
const match = await this.matchesIntegrityHash(patterns);
312306
const haveLockfile = await fs.exists(path.join(this.config.cwd, constants.LOCKFILE_FILENAME));
313307

314-
if (match.matches && haveLockfile) {
308+
if (match.integrityHashMatches && haveLockfile) {
315309
this.reporter.success(this.reporter.lang('upToDate'));
316310
return true;
317311
}
318312

319-
if (!patterns.length && !match.expected) {
313+
if (!patterns.length && !match.integrityFileMissing) {
320314
this.reporter.success(this.reporter.lang('nothingToInstall'));
321315
await this.createEmptyManifestFolders();
322316
await this.saveLockfileAndIntegrity(patterns);
@@ -398,8 +392,7 @@ export class Install {
398392

399393
steps.push(async (curr: number, total: number) => {
400394
// remove integrity hash to make this operation atomic
401-
const loc = await this.getIntegrityHashLocation();
402-
await fs.unlink(loc);
395+
await this.integrityChecker.removeIntegrityFile();
403396
this.reporter.step(curr, total, this.reporter.lang('linkingDependencies'), emoji.get('link'));
404397
await this.linker.init(patterns, this.flags.linkDuplicates);
405398
});
@@ -556,41 +549,26 @@ export class Install {
556549
return flattenedPatterns;
557550
}
558551

559-
/**
560-
* Check if the loaded lockfile has all the included patterns
561-
*/
562-
563-
lockFileInSync(patterns: Array<string>): boolean {
564-
let inSync = true;
565-
for (const pattern of patterns) {
566-
if (!this.lockfile.getLocked(pattern)) {
567-
inSync = false;
568-
break;
569-
}
570-
}
571-
return inSync;
572-
}
573-
574552
/**
575553
* Save updated integrity and lockfiles.
576554
*/
577555

578556
async saveLockfileAndIntegrity(patterns: Array<string>): Promise<void> {
579-
// stringify current lockfile
580-
const lockSource = lockStringify(this.lockfile.getLockfile(this.resolver.patterns));
581-
582-
// write integrity hash
583-
await this.writeIntegrityHash(lockSource, patterns);
584-
585557
// --no-lockfile or --pure-lockfile flag
586558
if (this.flags.lockfile === false || this.flags.pureLockfile) {
587559
return;
588560
}
589561

590-
const inSync = this.lockFileInSync(patterns);
562+
// stringify current lockfile
563+
const lockSource = lockStringify(this.lockfile.getLockfile(this.resolver.patterns));
591564

592-
// remove is followed by install with force on which we rewrite lockfile
593-
if (inSync && patterns.length && !this.flags.force) {
565+
// write integrity hash
566+
await this.integrityChecker.save(patterns, lockSource, this.flags, this.resolver.usedRegistries);
567+
568+
const lockFileHasAllPatterns = patterns.filter((p) => !this.lockfile.getLocked(p)).length === 0;
569+
570+
// remove command is followed by install with force, lockfile will be rewritten in any case then
571+
if (lockFileHasAllPatterns && patterns.length && !this.flags.force) {
594572
return;
595573
}
596574

@@ -607,111 +585,6 @@ export class Install {
607585
this.reporter.success(this.reporter.lang('savedLockfile'));
608586
}
609587

610-
/**
611-
* Check if the integrity hash of this installation matches one on disk.
612-
*/
613-
614-
async matchesIntegrityHash(patterns: Array<string>): Promise<IntegrityMatch> {
615-
const loc = await this.getIntegrityHashLocation();
616-
if (!await fs.exists(loc)) {
617-
return {
618-
actual: '',
619-
expected: '',
620-
loc,
621-
matches: false,
622-
};
623-
}
624-
625-
const lockSource = lockStringify(this.lockfile.getLockfile(this.resolver.patterns));
626-
const actual = this.generateIntegrityHash(lockSource, patterns);
627-
const expected = (await fs.readFile(loc)).trim();
628-
629-
return {
630-
actual,
631-
expected,
632-
loc,
633-
matches: actual === expected,
634-
};
635-
}
636-
637-
/**
638-
* Get the location of an existing integrity hash. If none exists then return the location where we should
639-
* write a new one.
640-
*/
641-
642-
async getIntegrityHashLocation(): Promise<string> {
643-
// build up possible folders
644-
const possibleFolders = [];
645-
if (this.config.modulesFolder) {
646-
possibleFolders.push(this.config.modulesFolder);
647-
}
648-
649-
// get a list of registry names to check existence in
650-
let checkRegistryNames = this.resolver.usedRegistries;
651-
if (!checkRegistryNames.length) {
652-
// we haven't used any registries yet
653-
checkRegistryNames = registryNames;
654-
}
655-
656-
// ensure we only write to a registry folder that was used
657-
for (const name of checkRegistryNames) {
658-
const loc = path.join(this.config.cwd, this.config.registries[name].folder);
659-
possibleFolders.push(loc);
660-
}
661-
662-
// if we already have an integrity hash in one of these folders then use it's location otherwise use the
663-
// first folder
664-
const possibles = possibleFolders.map((folder): string => path.join(folder, constants.INTEGRITY_FILENAME));
665-
let loc = possibles[0];
666-
for (const possibleLoc of possibles) {
667-
if (await fs.exists(possibleLoc)) {
668-
loc = possibleLoc;
669-
break;
670-
}
671-
}
672-
return loc;
673-
}
674-
/**
675-
* Write the integrity hash of the current install to disk.
676-
*/
677-
678-
async writeIntegrityHash(lockSource: string, patterns: Array<string>): Promise<void> {
679-
const loc = await this.getIntegrityHashLocation();
680-
invariant(loc, 'expected integrity hash location');
681-
await fs.mkdirp(path.dirname(loc));
682-
await fs.writeFile(loc, this.generateIntegrityHash(lockSource, patterns));
683-
}
684-
685-
/**
686-
* Generate integrity hash of input lockfile.
687-
*/
688-
689-
generateIntegrityHash(lockfile: string, patterns: Array<string>): string {
690-
const opts = [lockfile];
691-
692-
opts.push(`patterns:${patterns.sort(sortAlpha).join(',')}`);
693-
694-
if (this.flags.flat) {
695-
opts.push('flat');
696-
}
697-
698-
if (this.config.production) {
699-
opts.push('production');
700-
}
701-
702-
const linkedModules = this.config.linkedModules;
703-
if (linkedModules.length) {
704-
opts.push(`linked:${linkedModules.join(',')}`);
705-
}
706-
707-
const mirror = this.config.getOfflineMirrorPath();
708-
if (mirror != null) {
709-
opts.push(`mirror:${mirror}`);
710-
}
711-
712-
return crypto.hash(opts.join('-'), 'sha256');
713-
}
714-
715588
/**
716589
* Load the dependency graph of the current install. Only does package resolving and wont write to the cwd.
717590
*/

src/cli/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ commander.option('--har', 'save HAR output of network traffic');
5656
commander.option('--ignore-platform', 'ignore platform checks');
5757
commander.option('--ignore-engines', 'ignore engines check');
5858
commander.option('--ignore-optional', 'ignore optional dependencies');
59-
commander.option('--force', 'install and build scripts even if they were built before, overwrite lockfile');
59+
commander.option('--force', 'install and build packages even if they were built before, overwrite lockfile');
6060
commander.option('--skip-integrity-check', 'run install without checking if node_modules is installed');
6161
commander.option('--no-bin-links', "don't generate bin links when setting up packages");
6262
commander.option('--flat', 'only allow one version of a package');

0 commit comments

Comments
 (0)