Skip to content
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

[rush] Add support for RUSH_OVERRIDE_PATH environment variable #5003

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
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')) {
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
if (semver.lt(version, '4.0.0')) {
if (isLegacyRushVersion) {

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')) {
Copy link
Member

Choose a reason for hiding this comment

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

This condition is checked twice now.

Copy link
Member

Choose a reason for hiding this comment

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

Actually looks like it's checked in a few places.

// 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
82 changes: 66 additions & 16 deletions apps/rush/src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,90 @@ const alreadyReportedNodeTooNewError: boolean = NodeJsCompatibility.warnAboutVer
import * as os from 'os';
import * as semver from 'semver';

import { Text, PackageJsonLookup } from '@rushstack/node-core-library';
import { Colorize, ConsoleTerminalProvider, type ITerminalProvider } from '@rushstack/terminal';
import { Text, PackageJsonLookup, type IPackageJson } from '@rushstack/node-core-library';
import { PrintUtilities, Colorize, ConsoleTerminalProvider, type ITerminalProvider, Terminal } from '@rushstack/terminal';
import { EnvironmentVariableNames } from '@microsoft/rush-lib';
import * as rushLib from '@microsoft/rush-lib';

import { RushCommandSelector } from './RushCommandSelector';
import { RushVersionSelector } from './RushVersionSelector';
import { MinimalRushConfiguration } from './MinimalRushConfiguration';

const terminalProvider: ITerminalProvider = new ConsoleTerminalProvider();

const terminal: Terminal = new Terminal(terminalProvider);

// Load the configuration
const configuration: MinimalRushConfiguration | undefined =
MinimalRushConfiguration.loadFromDefaultLocation();

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

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

const overridePath: string | undefined = process.env[EnvironmentVariableNames.RUSH_OVERRIDE_PATH];

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

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

if (overridePackageJson === undefined) {
terminal.writeErrorLine(`Cannot use version specified with "RUSH_OVERRIDE_PATH" environment variable as it doesn't point to valid Rush package: ${overridePath}`);
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
terminal.writeErrorLine(`Cannot use version specified with "RUSH_OVERRIDE_PATH" environment variable as it doesn't point to valid Rush package: ${overridePath}`);
terminal.writeErrorLine(`Unable to load the a copy of `rush-lib` specified with ` +
`the "${EnvironmentVariableNames.RUSH_OVERRIDE_PATH}" environment ` +
`variable: ${overridePath}`);

process.exit(1);
}

const overrideVersion: string = overridePackageJson.version;

// If we are overriding with an older Rush that doesn't understand the RUSH_OVERRIDE_PATH variable,
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
// If we are overriding with an older Rush that doesn't understand the RUSH_OVERRIDE_PATH variable,
// If we are overriding with an older version of Rush that doesn't understand the RUSH_OVERRIDE_PATH variable,

// then unset it.
if (semver.lt(overrideVersion, '5.141.0')) {
delete process.env[EnvironmentVariableNames.RUSH_OVERRIDE_PATH];
Copy link
Member

Choose a reason for hiding this comment

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

Should we just always delete the env var here?

}

PrintUtilities.printMessageInBox(
[
`WARNING! THE "RUSH_OVERRIDE_PATH" ENVIRONMENT VARIABLE IS SET.`,
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
`WARNING! THE "RUSH_OVERRIDE_PATH" ENVIRONMENT VARIABLE IS SET.`,
`WARNING! THE "${EnvironmentVariableNames.RUSH_OVERRIDE_PATH}" ENVIRONMENT VARIABLE IS SET.`,

``,
`You are using Rush@${overrideVersion} from ${overridePath}`,
``,
...(
configuration
? [
`The rush.json configuration asks for: @${configuration.rushVersion}`,
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
`The rush.json configuration asks for: @${configuration.rushVersion}`,
`The ${RushConstants.rushJsonFilename} configuration asks for v${configuration.rushVersion}`,

``,
]
: []
),
`To restore the normal behavior, unset the "RUSH_OVERRIDE_PATH" environment variable.`,
].join(os.EOL),
terminal,
);

rushVersionToLoadInfo = {
version: overrideVersion,
path: overridePath,
};
} 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;
// If we are previewing an older Rush that doesn't understand the RUSH_PREVIEW_VERSION variable,
// then unset it.
if (semver.lt(previewVersion, '5.0.0-dev.18')) {
delete process.env[EnvironmentVariableNames.RUSH_PREVIEW_VERSION];
}

rushVersionToLoadInfo = {
version: previewVersion,
};

const lines: string[] = [];
lines.push(
Expand All @@ -71,28 +127,22 @@ if (previewVersion) {

console.error(lines.map((line) => Colorize.black(Colorize.yellowBackground(line))).join(os.EOL));
} else if (configuration) {
rushVersionToLoad = 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')) {
delete process.env[EnvironmentVariableNames.RUSH_PREVIEW_VERSION];
rushVersionToLoadInfo = {
version: configuration.rushVersion,
};
}

// Rush is "managed" if its version and configuration are dictated by a repo's rush.json
const isManaged: boolean = !!configuration;

const terminalProvider: ITerminalProvider = new ConsoleTerminalProvider();

const launchOptions: rushLib.ILaunchOptions = { isManaged, alreadyReportedNodeTooNewError, terminalProvider };

// 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 || rushVersionToLoadInfo.path !== undefined)) {
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 RUSH_OVERRIDE_PATH environment variable",
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
"comment": "Add support for overriding rush with custom installation path through RUSH_OVERRIDE_PATH environment variable",
"comment": "Add support for overriding Rush with a custom installation path through a new `RUSH_OVERRIDE_PATH` environment variable. This environment variable is intended to be used for development of Rush itself.",

"type": "none"
}
],
"packageName": "@microsoft/rush"
}
1 change: 1 addition & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ export class EnvironmentConfiguration {
export const EnvironmentVariableNames: {
readonly RUSH_TEMP_FOLDER: "RUSH_TEMP_FOLDER";
readonly RUSH_PREVIEW_VERSION: "RUSH_PREVIEW_VERSION";
readonly RUSH_OVERRIDE_PATH: "RUSH_OVERRIDE_PATH";
readonly RUSH_ALLOW_UNSUPPORTED_NODEJS: "RUSH_ALLOW_UNSUPPORTED_NODEJS";
readonly RUSH_ALLOW_WARNINGS_IN_SUCCESSFUL_BUILD: "RUSH_ALLOW_WARNINGS_IN_SUCCESSFUL_BUILD";
readonly RUSH_VARIANT: "RUSH_VARIANT";
Expand Down
6 changes: 6 additions & 0 deletions libraries/rush-lib/src/api/EnvironmentConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const EnvironmentVariableNames = {
*/
RUSH_PREVIEW_VERSION: 'RUSH_PREVIEW_VERSION',

/**
* This variable overrides the path of Rush that will used by the version selector.
*/
RUSH_OVERRIDE_PATH: 'RUSH_OVERRIDE_PATH',

/**
* If this variable is set to "1", Rush will not fail the build when running a version
* of Node that does not match the criteria specified in the "nodeSupportedVersionRange"
Expand Down Expand Up @@ -541,6 +546,7 @@ export class EnvironmentConfiguration {

case EnvironmentVariableNames.RUSH_PARALLELISM:
case EnvironmentVariableNames.RUSH_PREVIEW_VERSION:
case EnvironmentVariableNames.RUSH_OVERRIDE_PATH:
case EnvironmentVariableNames.RUSH_VARIANT:
case EnvironmentVariableNames.RUSH_DEPLOY_TARGET_FOLDER:
// Handled by @microsoft/rush front end
Expand Down
Loading