diff --git a/apps/rush/src/MinimalRushConfiguration.ts b/apps/rush/src/MinimalRushConfiguration.ts index 7078f4af812..ab793188044 100644 --- a/apps/rush/src/MinimalRushConfiguration.ts +++ b/apps/rush/src/MinimalRushConfiguration.ts @@ -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( @@ -52,6 +54,10 @@ export class MinimalRushConfiguration { } } + public get rushJsonFilename(): string { + return this._rushJsonFilename; + } + /** * 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 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..ae36b671f3a 100644 --- a/apps/rush/src/start.ts +++ b/apps/rush/src/start.ts @@ -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'; @@ -36,11 +38,67 @@ 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), + rushLib.RushConstants.commonFolderName, + rushLib.RushConstants.rushTempFolderName, + '.rush-override' + ); + + if (fs.existsSync(rushOverrideFilePath)) { + const overridePath: string = fs.readFileSync(rushOverrideFilePath, 'utf8').trim(); + 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`)); + 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( + `*********************************************************************`, + `* 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}"`) @@ -48,7 +106,9 @@ if (previewVersion) { process.exit(1); } - rushVersionToLoad = previewVersion; + rushVersionToLoadInfo = { + version: previewVersion, + }; const lines: string[] = []; lines.push( @@ -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]; } @@ -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)); }); diff --git a/common/changes/@microsoft/rush/rush-override-support_2024-11-09-01-59.json b/common/changes/@microsoft/rush/rush-override-support_2024-11-09-01-59.json new file mode 100644 index 00000000000..395c6ee3f99 --- /dev/null +++ b/common/changes/@microsoft/rush/rush-override-support_2024-11-09-01-59.json @@ -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" +} \ No newline at end of file