diff --git a/apps/rush/src/RushVersionSelector.ts b/apps/rush/src/RushVersionSelector.ts index b4ba82fb444..24ae179140c 100644 --- a/apps/rush/src/RushVersionSelector.ts +++ b/apps/rush/src/RushVersionSelector.ts @@ -24,52 +24,66 @@ export class RushVersionSelector { public async ensureRushVersionInstalledAsync( version: string, + overridePath: string | undefined, configuration: MinimalRushConfiguration | undefined, executeOptions: ILaunchOptions ): Promise { 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'); } } @@ -77,16 +91,16 @@ export class RushVersionSelector { // 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); } diff --git a/apps/rush/src/start.ts b/apps/rush/src/start.ts index 90f79a835d1..ab702da79bf 100644 --- a/apps/rush/src/start.ts +++ b/apps/rush/src/start.ts @@ -21,8 +21,8 @@ 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'; @@ -30,17 +30,65 @@ 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}`); + 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, + // then unset it. + if (semver.lt(overrideVersion, '5.141.0')) { + delete process.env[EnvironmentVariableNames.RUSH_OVERRIDE_PATH]; + } + + PrintUtilities.printMessageInBox( + [ + `WARNING! THE "RUSH_OVERRIDE_PATH" ENVIRONMENT VARIABLE IS SET.`, + ``, + `You are using Rush@${overrideVersion} from ${overridePath}`, + ``, + ...( + configuration + ? [ + `The rush.json configuration asks for: @${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}"`) @@ -48,7 +96,15 @@ if (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( @@ -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)); }); diff --git a/common/changes/@microsoft/rush/rush-override-support-env_2024-11-14-23-12.json b/common/changes/@microsoft/rush/rush-override-support-env_2024-11-14-23-12.json new file mode 100644 index 00000000000..187948985ae --- /dev/null +++ b/common/changes/@microsoft/rush/rush-override-support-env_2024-11-14-23-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for overriding rush with custom installation path through RUSH_OVERRIDE_PATH environment variable", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 454ac3daa07..bd70476431e 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -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"; diff --git a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts index da883ddff60..d435439ac53 100644 --- a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts +++ b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts @@ -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" @@ -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