mirror of
https://github.com/zhigang1992/yarn.git
synced 2026-06-10 07:55:57 +08:00
**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) ```
446 lines
13 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|