Skip to content

[rush] Add support for common/temp/.rush-override #5000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/rush/src/MinimalRushConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ interface IMinimalRushConfigurationJson {
* decide which version of Rush should be installed/used.
*/
export class MinimalRushConfiguration {
private _rushJsonFilename: string;
private _rushVersion: string;
private _commonRushConfigFolder: string;

private constructor(minimalRushConfigurationJson: IMinimalRushConfigurationJson, rushJsonFilename: string) {
this._rushJsonFilename = rushJsonFilename;
this._rushVersion =
minimalRushConfigurationJson.rushVersion || minimalRushConfigurationJson.rushMinimumVersion;
this._commonRushConfigFolder = path.join(
Expand Down Expand Up @@ -52,6 +54,10 @@ export class MinimalRushConfiguration {
}
}

public get rushJsonFilename(): string {
return this._rushJsonFilename;
}
Comment on lines +57 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.


/**
* The version of rush specified by the rushVersion property of the rush.json configuration file. If the
* rushVersion property is not specified, this falls back to the rushMinimumVersion property. This should be
Expand Down
96 changes: 55 additions & 41 deletions apps/rush/src/RushVersionSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,69 +24,83 @@ export class RushVersionSelector {

public async ensureRushVersionInstalledAsync(
version: string,
overridePath: string | undefined,
configuration: MinimalRushConfiguration | undefined,
executeOptions: ILaunchOptions
): Promise<void> {
const isLegacyRushVersion: boolean = semver.lt(version, '4.0.0');
const expectedRushPath: string = path.join(this._rushGlobalFolder.nodeSpecificPath, `rush-${version}`);

const installMarker: _FlagFile = new _FlagFile(expectedRushPath, 'last-install', {
node: process.versions.node
});
let rushPath: string;

let installIsValid: boolean = await installMarker.isValidAsync();
if (!installIsValid) {
// Need to install Rush
console.log(`Rush version ${version} is not currently installed. Installing...`);

const resourceName: string = `rush-${version}`;

console.log(`Trying to acquire lock for ${resourceName}`);
if (overridePath) {
rushPath = overridePath;
} else {
const expectedRushPath: string = path.join(this._rushGlobalFolder.nodeSpecificPath, `rush-${version}`);

const installMarker: _FlagFile = new _FlagFile(expectedRushPath, 'last-install', {
node: process.versions.node
});

let installIsValid: boolean = await installMarker.isValidAsync();
if (!installIsValid) {
// Need to install Rush
console.log(`Rush version ${version} is not currently installed. Installing...`);

const resourceName: string = `rush-${version}`;

console.log(`Trying to acquire lock for ${resourceName}`);

const lock: LockFile = await LockFile.acquire(expectedRushPath, resourceName);
installIsValid = await installMarker.isValidAsync();
if (installIsValid) {
console.log('Another process performed the installation.');
} else {
await Utilities.installPackageInDirectoryAsync({
directory: expectedRushPath,
packageName: isLegacyRushVersion ? '@microsoft/rush' : '@microsoft/rush-lib',
version: version,
tempPackageTitle: 'rush-local-install',
maxInstallAttempts: MAX_INSTALL_ATTEMPTS,
// This is using a local configuration to install a package in a shared global location.
// Generally that's a bad practice, but in this case if we can successfully install
// the package at all, we can reasonably assume it's good for all the repositories.
// In particular, we'll assume that two different NPM registries cannot have two
// different implementations of the same version of the same package.
// This was needed for: https://github.com/microsoft/rushstack/issues/691
commonRushConfigFolder: configuration ? configuration.commonRushConfigFolder : undefined,
suppressOutput: true
});

console.log(`Successfully installed Rush version ${version} in ${expectedRushPath}.`);

// If we've made it here without exception, write the flag file
await installMarker.createAsync();

lock.release();
}
}

const lock: LockFile = await LockFile.acquire(expectedRushPath, resourceName);
installIsValid = await installMarker.isValidAsync();
if (installIsValid) {
console.log('Another process performed the installation.');
if (semver.lt(version, '4.0.0')) {
rushPath = path.join(expectedRushPath, 'node_modules', '@microsoft', 'rush');
} else {
await Utilities.installPackageInDirectoryAsync({
directory: expectedRushPath,
packageName: isLegacyRushVersion ? '@microsoft/rush' : '@microsoft/rush-lib',
version: version,
tempPackageTitle: 'rush-local-install',
maxInstallAttempts: MAX_INSTALL_ATTEMPTS,
// This is using a local configuration to install a package in a shared global location.
// Generally that's a bad practice, but in this case if we can successfully install
// the package at all, we can reasonably assume it's good for all the repositories.
// In particular, we'll assume that two different NPM registries cannot have two
// different implementations of the same version of the same package.
// This was needed for: https://github.com/microsoft/rushstack/issues/691
commonRushConfigFolder: configuration ? configuration.commonRushConfigFolder : undefined,
suppressOutput: true
});

console.log(`Successfully installed Rush version ${version} in ${expectedRushPath}.`);

// If we've made it here without exception, write the flag file
await installMarker.createAsync();

lock.release();
rushPath = path.join(expectedRushPath, 'node_modules', '@microsoft', 'rush-lib');
}
}

if (semver.lt(version, '3.0.20')) {
// In old versions, requiring the entry point invoked the command-line parser immediately,
// so fail if "rushx" or "rush-pnpm" was used
RushCommandSelector.failIfNotInvokedAsRush(version);
require(path.join(expectedRushPath, 'node_modules', '@microsoft', 'rush', 'lib', 'rush'));
require(path.join(rushPath, 'lib', 'rush'));
} else if (semver.lt(version, '4.0.0')) {
// In old versions, requiring the entry point invoked the command-line parser immediately,
// so fail if "rushx" or "rush-pnpm" was used
RushCommandSelector.failIfNotInvokedAsRush(version);
require(path.join(expectedRushPath, 'node_modules', '@microsoft', 'rush', 'lib', 'start'));
require(path.join(rushPath, 'lib', 'start'));
} else {
// For newer rush-lib, RushCommandSelector can test whether "rushx" is supported or not
const rushCliEntrypoint: {} = require(
path.join(expectedRushPath, 'node_modules', '@microsoft', 'rush-lib', 'lib', 'index')
path.join(rushPath, 'lib', 'index')
);
RushCommandSelector.execute(this._currentPackageVersion, rushCliEntrypoint, executeOptions);
}
Expand Down
78 changes: 70 additions & 8 deletions apps/rush/src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ const alreadyReportedNodeTooNewError: boolean = NodeJsCompatibility.warnAboutVer
alreadyReportedNodeTooNewError: false
});

import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import * as semver from 'semver';

import { Text, PackageJsonLookup } from '@rushstack/node-core-library';
import { Text, PackageJsonLookup, type IPackageJson } from '@rushstack/node-core-library';
import { Colorize, ConsoleTerminalProvider, type ITerminalProvider } from '@rushstack/terminal';
import { EnvironmentVariableNames } from '@microsoft/rush-lib';
import * as rushLib from '@microsoft/rush-lib';
Expand All @@ -36,19 +38,77 @@ const configuration: MinimalRushConfiguration | undefined =

const currentPackageVersion: string = PackageJsonLookup.loadOwnPackageJson(__dirname).version;

let rushVersionToLoad: string | undefined = undefined;
let rushVersionToLoadInfo: {
version: string;
path?: string;
} | undefined = undefined;

let overrideInfo: {
version: string;
path: string;
} | undefined = undefined;

if (configuration) {
const rushOverrideFilePath: string = path.join(
path.dirname(configuration.rushJsonFilename),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
path.dirname(configuration.rushJsonFilename),
configuration.rushJsonFolder,

rushLib.RushConstants.commonFolderName,
rushLib.RushConstants.rushTempFolderName,
'.rush-override'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stick this in a constant.

);
Comment on lines +52 to +57
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const rushOverrideFilePath: string = path.join(
path.dirname(configuration.rushJsonFilename),
rushLib.RushConstants.commonFolderName,
rushLib.RushConstants.rushTempFolderName,
'.rush-override'
);
const rushOverrideFilePath: string = `${configuration.rushJsonFolder}/.rush-override`;

As per discussion.

Also stick .rush-override in a RushConstants.


if (fs.existsSync(rushOverrideFilePath)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use FileSystem from node-core-library, and do a try-catch on reading the file and gracefully handle it not existing. See FileSystem.isNotExistError

const overridePath: string = fs.readFileSync(rushOverrideFilePath, 'utf8').trim();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use FileSystem.readFile

const overridePackageJson: IPackageJson | undefined = PackageJsonLookup.instance.tryLoadPackageJsonFor(overridePath);

if (overridePackageJson === undefined) {
console.log(Colorize.red(`Cannot use common/temp/.rush-override file as it doesn't point to valid Rush package`));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use constants here.

console.log(``);
console.log(Colorize.red(`If you're unfamiliar with this file, you can safely delete it`));
process.exit(1);
}

const overrideVersion: string = overridePackageJson.version;

const lines: string[] = [];
lines.push(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a printInBox utility function in @rushstack/terminal.

`*********************************************************************`,
`* WARNING! THE "common/temp/.rush-override" FILE IS PRESENT. *`,
`* *`,
`* You are using Rush@${overrideVersion} from .rush-override${Text.padEnd('', 26-overrideVersion.length)} *`
);

lines.push(`* The rush.json configuration asks for: ${Text.padEnd(configuration.rushVersion, 25)} *`);

lines.push(
`* *`,
`* To restore the normal behavior, delete common/temp/.rush-override *`,
`*********************************************************************`
);

console.error(lines.map((line) => Colorize.black(Colorize.yellowBackground(line))).join(os.EOL));

overrideInfo = {
version: overrideVersion,
path: overridePath,
};
}
}

const previewVersion: string | undefined = process.env[EnvironmentVariableNames.RUSH_PREVIEW_VERSION];

if (previewVersion) {
if (overrideInfo) {
rushVersionToLoadInfo = overrideInfo;
} else if (previewVersion) {
if (!semver.valid(previewVersion, false)) {
console.error(
Colorize.red(`Invalid value for RUSH_PREVIEW_VERSION environment variable: "${previewVersion}"`)
);
process.exit(1);
}

rushVersionToLoad = previewVersion;
rushVersionToLoadInfo = {
version: previewVersion,
};

const lines: string[] = [];
lines.push(
Expand All @@ -71,12 +131,14 @@ if (previewVersion) {

console.error(lines.map((line) => Colorize.black(Colorize.yellowBackground(line))).join(os.EOL));
} else if (configuration) {
rushVersionToLoad = configuration.rushVersion;
rushVersionToLoadInfo = {
version: configuration.rushVersion,
};
}

// If we are previewing an older Rush that doesn't understand the RUSH_PREVIEW_VERSION variable,
// then unset it.
if (rushVersionToLoad && semver.lt(rushVersionToLoad, '5.0.0-dev.18')) {
if (rushVersionToLoadInfo && semver.lt(rushVersionToLoadInfo.version, '5.0.0-dev.18')) {
delete process.env[EnvironmentVariableNames.RUSH_PREVIEW_VERSION];
}

Expand All @@ -89,10 +151,10 @@ const launchOptions: rushLib.ILaunchOptions = { isManaged, alreadyReportedNodeTo

// If we're inside a repo folder, and it's requesting a different version, then use the RushVersionManager to
// install it
if (rushVersionToLoad && rushVersionToLoad !== currentPackageVersion) {
if (rushVersionToLoadInfo && rushVersionToLoadInfo.version !== currentPackageVersion) {
const versionSelector: RushVersionSelector = new RushVersionSelector(currentPackageVersion);
versionSelector
.ensureRushVersionInstalledAsync(rushVersionToLoad, configuration, launchOptions)
.ensureRushVersionInstalledAsync(rushVersionToLoadInfo.version, rushVersionToLoadInfo.path, configuration, launchOptions)
.catch((error: Error) => {
console.log(Colorize.red('Error: ' + error.message));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add support for overriding rush with custom installation path through common/temp/.rush-override file",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
Loading