Files
yarn/src/integrity-checker.js
Roman Schejbal 917713d556 fix(install): Rebuild native modules when node version changes (#4750)
**Summary**

Fixes #756. We have multiple versions of our app and each one uses a different version of node. 
Therefore we need to rebuild our `node-sass` module every time we move from one to another. 

This PR addresses that by saving the NODE version those artifacts were built with within the `.yarn-integrity` file and triggers forced scripts install (only if the node version is different ofc).

**Test plan**

```
1. Install Node.js 7.x
2. Add the node-sass dependency to the project via Yarn
3. Update Node.js to 8.x (new NODE_VERSION)
4. Run "yarn install" (you should see yarn downloading fresh scripts/binaries)
```
2017-10-26 03:18:23 -07:00

446 lines
13 KiB
JavaScript

/* @flow */
import type Config from './config.js';
import type {LockManifest} from './lockfile';
import * as constants from './constants.js';
import * as fs from './util/fs.js';
import {sortAlpha, compareSortedArrays} from './util/misc.js';
import type {InstallArtifacts} from './package-install-scripts.js';
import WorkspaceLayout from './workspace-layout.js';
const invariant = require('invariant');
const path = require('path');
export const integrityErrors = {
EXPECTED_IS_NOT_A_JSON: 'integrityFailedExpectedIsNotAJSON',
FILES_MISSING: 'integrityFailedFilesMissing',
LOCKFILE_DONT_MATCH: 'integrityLockfilesDontMatch',
FLAGS_DONT_MATCH: 'integrityFlagsDontMatch',
LINKED_MODULES_DONT_MATCH: 'integrityCheckLinkedModulesDontMatch',
PATTERNS_DONT_MATCH: 'integrityPatternsDontMatch',
MODULES_FOLDERS_MISSING: 'integrityModulesFoldersMissing',
NODE_VERSION_DOESNT_MATCH: 'integrityNodeDoesntMatch',
};
type IntegrityError = $Keys<typeof integrityErrors>;
export type IntegrityCheckResult = {
integrityFileMissing: boolean,
integrityMatches?: boolean,
integrityError?: IntegrityError,
missingPatterns: Array<string>,
};
type IntegrityHashLocation = {
locationFolder: string,
locationPath: string,
exists: boolean,
};
type IntegrityFile = {
nodeVersion: string,
flags: Array<string>,
modulesFolders: Array<string>,
linkedModules: Array<string>,
topLevelPatterns: Array<string>,
lockfileEntries: {
[key: string]: string,
},
files: Array<string>,
artifacts: ?InstallArtifacts,
};
type IntegrityFlags = {
flat: boolean,
checkFiles: boolean,
};
const INTEGRITY_FILE_DEFAULTS = () => ({
nodeVersion: process.version,
modulesFolders: [],
flags: [],
linkedModules: [],
topLevelPatterns: [],
lockfileEntries: {},
files: [],
});
/**
*
*/
export default class InstallationIntegrityChecker {
constructor(config: Config) {
this.config = config;
}
config: Config;
/**
* Get the common ancestor of every node_modules - it may be a node_modules directory itself, but isn't required to.
*/
_getModulesRootFolder(): string {
if (this.config.modulesFolder) {
return this.config.modulesFolder;
} else if (this.config.workspaceRootFolder) {
return this.config.workspaceRootFolder;
} else {
return path.join(this.config.lockfileFolder, constants.NODE_MODULES_FOLDER);
}
}
/**
* Get the directory in which the yarn-integrity file should be written.
*/
_getIntegrityFileFolder(): string {
if (this.config.modulesFolder) {
return this.config.modulesFolder;
} else if (this.config.enableMetaFolder) {
return path.join(this.config.lockfileFolder, constants.META_FOLDER);
} else {
return path.join(this.config.lockfileFolder, constants.NODE_MODULES_FOLDER);
}
}
/**
* Get the full path of the yarn-integrity file.
*/
async _getIntegrityFileLocation(): Promise<IntegrityHashLocation> {
const locationFolder = this._getIntegrityFileFolder();
const locationPath = path.join(locationFolder, constants.INTEGRITY_FILENAME);
const exists = await fs.exists(locationPath);
return {
locationFolder,
locationPath,
exists,
};
}
/**
* Get the list of the directories that contain our modules (there might be multiple such folders b/c of workspaces).
*/
_getModulesFolders({workspaceLayout}: {workspaceLayout: ?WorkspaceLayout} = {}): Array<string> {
const locations = [];
if (this.config.modulesFolder) {
locations.push(this.config.modulesFolder);
} else {
locations.push(path.join(this.config.lockfileFolder, constants.NODE_MODULES_FOLDER));
}
if (workspaceLayout) {
for (const workspaceName of Object.keys(workspaceLayout.workspaces)) {
const loc = workspaceLayout.workspaces[workspaceName].loc;
if (loc) {
locations.push(path.join(loc, constants.NODE_MODULES_FOLDER));
}
}
}
return locations.sort(sortAlpha);
}
/**
* Get a list of the files that are located inside our module folders.
*/
async _getIntegrityListing({workspaceLayout}: {workspaceLayout: ?WorkspaceLayout} = {}): Promise<Array<string>> {
const files = [];
const recurse = async dir => {
for (const file of await fs.readdir(dir)) {
const entry = path.join(dir, file);
const stat = await fs.lstat(entry);
if (stat.isDirectory()) {
await recurse(entry);
} else {
files.push(entry);
}
}
};
for (const modulesFolder of this._getModulesFolders({workspaceLayout})) {
if (await fs.exists(modulesFolder)) {
await recurse(modulesFolder);
}
}
return files;
}
/**
* Generate integrity hash of input lockfile.
*/
async _generateIntegrityFile(
lockfile: {[key: string]: LockManifest},
patterns: Array<string>,
flags: IntegrityFlags,
workspaceLayout: ?WorkspaceLayout,
artifacts?: InstallArtifacts,
): Promise<IntegrityFile> {
const result: IntegrityFile = {
...INTEGRITY_FILE_DEFAULTS(),
artifacts,
};
result.topLevelPatterns = patterns;
// If using workspaces, we also need to add the workspaces patterns to the top-level, so that we'll know if a
// dependency is added or removed into one of them. We must take care not to read the aggregator (if !loc).
//
// Also note that we can't use of workspaceLayout.workspaces[].manifest._reference.patterns, because when
// doing a "yarn check", the _reference property hasn't yet been properly initialized.
if (workspaceLayout) {
result.topLevelPatterns = result.topLevelPatterns.filter(p => {
// $FlowFixMe
return !workspaceLayout.getManifestByPattern(p);
});
for (const name of Object.keys(workspaceLayout.workspaces)) {
if (!workspaceLayout.workspaces[name].loc) {
continue;
}
const manifest = workspaceLayout.workspaces[name].manifest;
if (manifest) {
for (const dependencyType of constants.DEPENDENCY_TYPES) {
const dependencies = manifest[dependencyType];
if (!dependencies) {
continue;
}
for (const dep of Object.keys(dependencies)) {
result.topLevelPatterns.push(`${dep}@${dependencies[dep]}`);
}
}
}
}
}
result.topLevelPatterns.sort(sortAlpha);
if (flags.checkFiles) {
result.flags.push('checkFiles');
}
if (flags.flat) {
result.flags.push('flat');
}
if (flags.ignoreScripts) {
result.flags.push('ignoreScripts');
}
if (this.config.production) {
result.flags.push('production');
}
const linkedModules = this.config.linkedModules;
if (linkedModules.length) {
result.linkedModules = linkedModules.sort(sortAlpha);
}
for (const key of Object.keys(lockfile)) {
result.lockfileEntries[key] = lockfile[key].resolved || '';
}
for (const modulesFolder of this._getModulesFolders({workspaceLayout})) {
if (await fs.exists(modulesFolder)) {
result.modulesFolders.push(path.relative(this.config.lockfileFolder, modulesFolder));
}
}
if (flags.checkFiles) {
const modulesRoot = this._getModulesRootFolder();
result.files = (await this._getIntegrityListing({workspaceLayout}))
.map(entry => path.relative(modulesRoot, entry))
.sort(sortAlpha);
}
return result;
}
async _getIntegrityFile(locationPath: string): Promise<?IntegrityFile> {
const expectedRaw = await fs.readFile(locationPath);
try {
return {
...INTEGRITY_FILE_DEFAULTS(),
...JSON.parse(expectedRaw),
};
} catch (e) {
// ignore JSON parsing for legacy text integrity files compatibility
}
return null;
}
_compareIntegrityFiles(
actual: IntegrityFile,
expected: ?IntegrityFile,
checkFiles: boolean,
workspaceLayout: ?WorkspaceLayout,
): 'OK' | IntegrityError {
if (!expected) {
return 'EXPECTED_IS_NOT_A_JSON';
}
if (!compareSortedArrays(actual.linkedModules, expected.linkedModules)) {
return 'LINKED_MODULES_DONT_MATCH';
}
if (actual.nodeVersion !== expected.nodeVersion) {
return 'NODE_VERSION_DOESNT_MATCH';
}
let relevantExpectedFlags = expected.flags.slice();
// If we run "yarn" after "yarn --check-files", we shouldn't fail the less strict validation
if (actual.flags.indexOf('checkFiles') === -1) {
relevantExpectedFlags = relevantExpectedFlags.filter(flag => flag !== 'checkFiles');
}
if (!compareSortedArrays(actual.flags, relevantExpectedFlags)) {
return 'FLAGS_DONT_MATCH';
}
if (!compareSortedArrays(actual.topLevelPatterns, expected.topLevelPatterns || [])) {
return 'PATTERNS_DONT_MATCH';
}
for (const key of Object.keys(actual.lockfileEntries)) {
if (actual.lockfileEntries[key] !== expected.lockfileEntries[key]) {
return 'LOCKFILE_DONT_MATCH';
}
}
for (const key of Object.keys(expected.lockfileEntries)) {
if (actual.lockfileEntries[key] !== expected.lockfileEntries[key]) {
return 'LOCKFILE_DONT_MATCH';
}
}
if (checkFiles) {
// Early bailout if we expect more files than what we have
if (expected.files.length > actual.files.length) {
return 'FILES_MISSING';
}
// Since we know the "files" array is sorted (alphabetically), we can optimize the thing
// Instead of storing the files in a Set, we can just iterate both arrays at once. O(n)!
for (let u = 0, v = 0; u < expected.files.length; ++u) {
// Index that, if reached, means that we won't have enough food to match the remaining expected entries anyway
const max = v + (actual.files.length - v) - (expected.files.length - u) + 1;
// Skip over files that have been added (ie not present in 'expected')
while (v < max && actual.files[v] !== expected.files[u]) {
v += 1;
}
// If we've reached the index defined above, the file is either missing or we can early exit
if (v === max) {
return 'FILES_MISSING';
}
}
}
return 'OK';
}
async check(
patterns: Array<string>,
lockfile: {[key: string]: LockManifest},
flags: IntegrityFlags,
workspaceLayout: ?WorkspaceLayout,
): Promise<IntegrityCheckResult> {
// check if patterns exist in lockfile
const missingPatterns = patterns.filter(
p => !lockfile[p] && (!workspaceLayout || !workspaceLayout.getManifestByPattern(p)),
);
const loc = await this._getIntegrityFileLocation();
if (missingPatterns.length || !loc.exists) {
return {
integrityFileMissing: !loc.exists,
missingPatterns,
};
}
const actual = await this._generateIntegrityFile(lockfile, patterns, flags, workspaceLayout);
const expected = await this._getIntegrityFile(loc.locationPath);
let integrityMatches = this._compareIntegrityFiles(actual, expected, flags.checkFiles, workspaceLayout);
if (integrityMatches === 'OK') {
invariant(expected, "The integrity shouldn't pass without integrity file");
for (const modulesFolder of expected.modulesFolders) {
if (!await fs.exists(path.join(this.config.lockfileFolder, modulesFolder))) {
integrityMatches = 'MODULES_FOLDERS_MISSING';
}
}
}
return {
integrityFileMissing: false,
integrityMatches: integrityMatches === 'OK',
integrityError: integrityMatches === 'OK' ? undefined : integrityMatches,
missingPatterns,
hardRefreshRequired: integrityMatches === 'NODE_VERSION_DOESNT_MATCH',
};
}
/**
* Get artifacts from integrity file if it exists.
*/
async getArtifacts(): Promise<?InstallArtifacts> {
const loc = await this._getIntegrityFileLocation();
if (!loc.exists) {
return null;
}
const expectedRaw = await fs.readFile(loc.locationPath);
let expected: ?IntegrityFile;
try {
expected = JSON.parse(expectedRaw);
} catch (e) {
// ignore JSON parsing for legacy text integrity files compatibility
}
return expected ? expected.artifacts : null;
}
/**
* Write the integrity hash of the current install to disk.
*/
async save(
patterns: Array<string>,
lockfile: {[key: string]: LockManifest},
flags: IntegrityFlags,
workspaceLayout: ?WorkspaceLayout,
artifacts: InstallArtifacts,
): Promise<void> {
const integrityFile = await this._generateIntegrityFile(lockfile, patterns, flags, workspaceLayout, artifacts);
const loc = await this._getIntegrityFileLocation();
invariant(loc.locationPath, 'expected integrity hash location');
await fs.mkdirp(path.dirname(loc.locationPath));
await fs.writeFile(loc.locationPath, JSON.stringify(integrityFile, null, 2));
}
async removeIntegrityFile(): Promise<void> {
const loc = await this._getIntegrityFileLocation();
if (loc.exists) {
await fs.unlink(loc.locationPath);
}
}
}