diff --git a/apps/rush-lib/src/api/BuildCacheConfiguration.ts b/apps/rush-lib/src/api/BuildCacheConfiguration.ts index 225c6534f4a..cf083bc252d 100644 --- a/apps/rush-lib/src/api/BuildCacheConfiguration.ts +++ b/apps/rush-lib/src/api/BuildCacheConfiguration.ts @@ -11,8 +11,8 @@ import { } from '../logic/buildCache/AzureStorageBuildCacheProvider'; import { RushConfiguration } from './RushConfiguration'; import { FileSystemBuildCacheProvider } from '../logic/buildCache/FileSystemBuildCacheProvider'; -import { RushGlobalFolder } from './RushGlobalFolder'; import { RushConstants } from '../logic/RushConstants'; +import { RushUserConfiguration } from './RushUserConfiguration'; /** * Describes the file structure for the "common/config/rush/build-cache.json" config file. @@ -64,6 +64,12 @@ interface IFileSystemBuildCacheJson extends IBuildCacheJson { cacheProvider: 'filesystem'; } +interface IBuildCacheConfigurationOptions { + buildCacheJson: IBuildCacheJson; + rushConfiguration: RushConfiguration; + rushUserConfiguration: RushUserConfiguration; +} + /** * Use this class to load and save the "common/config/rush/build-cache.json" config file. * This file provides configuration options for cached project build output. @@ -78,17 +84,15 @@ export class BuildCacheConfiguration { public readonly cacheProvider: BuildCacheProviderBase; - private constructor( - buildCacheJson: IBuildCacheJson, - rushConfiguration: RushConfiguration, - rushGlobalFolder: RushGlobalFolder - ) { + private constructor(options: IBuildCacheConfigurationOptions) { + const { buildCacheJson, rushConfiguration, rushUserConfiguration } = options; this.projectOutputFolderNames = buildCacheJson.projectOutputFolderNames; switch (buildCacheJson.cacheProvider) { case 'filesystem': { this.cacheProvider = new FileSystemBuildCacheProvider({ - rushConfiguration + rushConfiguration, + rushUserConfiguration }); break; } @@ -98,7 +102,6 @@ export class BuildCacheConfiguration { const azureStorageConfigurationJson: IAzureStorageConfigurationJson = azureStorageBuildCacheJson.azureBlobStorageConfiguration; this.cacheProvider = new AzureStorageBuildCacheProvider({ - rushGlobalFolder, storageAccountName: azureStorageConfigurationJson.storageAccountName, storageContainerName: azureStorageConfigurationJson.storageContainerName, azureEnvironment: azureStorageConfigurationJson.azureEnvironment, @@ -119,8 +122,7 @@ export class BuildCacheConfiguration { * If the file has not been created yet, then undefined is returned. */ public static async loadFromDefaultPathAsync( - rushConfiguration: RushConfiguration, - rushGlobalFolder: RushGlobalFolder + rushConfiguration: RushConfiguration ): Promise { const jsonFilePath: string = BuildCacheConfiguration.getBuildCacheConfigFilePath(rushConfiguration); if (FileSystem.exists(jsonFilePath)) { @@ -128,7 +130,12 @@ export class BuildCacheConfiguration { jsonFilePath, BuildCacheConfiguration._jsonSchema ); - return new BuildCacheConfiguration(buildCacheJson, rushConfiguration, rushGlobalFolder); + const rushUserConfiguration: RushUserConfiguration = await RushUserConfiguration.initializeAsync(); + return new BuildCacheConfiguration({ + buildCacheJson, + rushConfiguration, + rushUserConfiguration + }); } else { return undefined; } diff --git a/apps/rush-lib/src/api/RushGlobalFolder.ts b/apps/rush-lib/src/api/RushGlobalFolder.ts index caf73315be1..f2e96f496d5 100644 --- a/apps/rush-lib/src/api/RushGlobalFolder.ts +++ b/apps/rush-lib/src/api/RushGlobalFolder.ts @@ -52,7 +52,7 @@ export class RushGlobalFolder { if (rushGlobalFolderOverride !== undefined) { this._rushGlobalFolder = rushGlobalFolderOverride; } else { - this._rushGlobalFolder = path.join(Utilities.getHomeDirectory(), '.rush'); + this._rushGlobalFolder = path.join(Utilities.getHomeFolder(), '.rush'); } const normalizedNodeVersion: string = process.version.match(/^[a-z0-9\-\.]+$/i) diff --git a/apps/rush-lib/src/api/RushUserConfiguration.ts b/apps/rush-lib/src/api/RushUserConfiguration.ts new file mode 100644 index 00000000000..42cd0a161c9 --- /dev/null +++ b/apps/rush-lib/src/api/RushUserConfiguration.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library'; +import * as path from 'path'; + +import { Utilities } from '../utilities/Utilities'; +import { RushConstants } from '../logic/RushConstants'; + +interface IRushUserSettingsJson { + buildCacheFolder?: string; +} + +/** + * Rush per-user configuration data. + * + * @beta + */ +export class RushUserConfiguration { + private static _schema: JsonSchema = JsonSchema.fromFile( + path.resolve(__dirname, '..', 'schemas', 'rush-user-settings.schema.json') + ); + + /** + * If provided, store build cache in the specified folder. Must be an absolute path. + */ + public readonly buildCacheFolder: string | undefined; + + private constructor(rushUserConfigurationJson: IRushUserSettingsJson | undefined) { + this.buildCacheFolder = rushUserConfigurationJson?.buildCacheFolder; + if (this.buildCacheFolder && !path.isAbsolute(this.buildCacheFolder)) { + throw new Error('buildCacheFolder must be an absolute path'); + } + } + + public static async initializeAsync(): Promise { + const rushUserFolderPath: string = RushUserConfiguration.getRushUserFolderPath(); + const rushUserSettingsFilePath: string = path.join(rushUserFolderPath, 'settings.json'); + let rushUserSettingsJson: IRushUserSettingsJson | undefined; + try { + rushUserSettingsJson = await JsonFile.loadAndValidateAsync( + rushUserSettingsFilePath, + RushUserConfiguration._schema + ); + } catch (e) { + if (!FileSystem.isNotExistError(e)) { + throw e; + } + } + + return new RushUserConfiguration(rushUserSettingsJson); + } + + public static getRushUserFolderPath(): string { + const homeFolderPath: string = Utilities.getHomeFolder(); + const rushUserSettingsFilePath: string = path.join( + homeFolderPath, + RushConstants.rushUserConfigurationFolderName + ); + return rushUserSettingsFilePath; + } +} diff --git a/apps/rush-lib/src/cli/actions/UpdateCloudCredentials.ts b/apps/rush-lib/src/cli/actions/UpdateCloudCredentials.ts index 60ca7502e7f..b76725c90cf 100644 --- a/apps/rush-lib/src/cli/actions/UpdateCloudCredentials.ts +++ b/apps/rush-lib/src/cli/actions/UpdateCloudCredentials.ts @@ -56,10 +56,7 @@ export class UpdateCloudCredentials extends BaseRushAction { const buildCacheConfiguration: | BuildCacheConfiguration - | undefined = await BuildCacheConfiguration.loadFromDefaultPathAsync( - this.rushConfiguration, - this.rushGlobalFolder - ); + | undefined = await BuildCacheConfiguration.loadFromDefaultPathAsync(this.rushConfiguration); if (!buildCacheConfiguration) { const buildCacheConfigurationFilePath: string = BuildCacheConfiguration.getBuildCacheConfigFilePath( diff --git a/apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts b/apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts index 998d620fead..d05bd5aaea9 100644 --- a/apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts +++ b/apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts @@ -112,10 +112,7 @@ export class BulkScriptAction extends BaseScriptAction { const buildCacheConfiguration: | BuildCacheConfiguration - | undefined = await BuildCacheConfiguration.loadFromDefaultPathAsync( - this.rushConfiguration, - this.rushGlobalFolder - ); + | undefined = await BuildCacheConfiguration.loadFromDefaultPathAsync(this.rushConfiguration); const taskSelector: TaskSelector = new TaskSelector({ rushConfiguration: this.rushConfiguration, diff --git a/apps/rush-lib/src/logic/CredentialCache.ts b/apps/rush-lib/src/logic/CredentialCache.ts index 27585936044..8e6c11f2e84 100644 --- a/apps/rush-lib/src/logic/CredentialCache.ts +++ b/apps/rush-lib/src/logic/CredentialCache.ts @@ -4,8 +4,8 @@ import * as path from 'path'; import { FileSystem, JsonFile, JsonSchema, LockFile } from '@rushstack/node-core-library'; -import { RushGlobalFolder } from '../api/RushGlobalFolder'; import { IDisposable, Utilities } from '../utilities/Utilities'; +import { RushUserConfiguration } from '../api/RushUserConfiguration'; const CACHE_FILENAME: string = 'credentials.json'; const LATEST_CREDENTIALS_JSON_VERSION: string = '0.1.0'; @@ -28,7 +28,6 @@ export interface ICredentialCacheEntry { } export interface ICredentialCacheOptions { - rushGlobalFolder: RushGlobalFolder; supportEditing: boolean; } @@ -59,8 +58,8 @@ export class CredentialCache implements IDisposable { } public static async initializeAsync(options: ICredentialCacheOptions): Promise { - const rushGlobalFolderPath: string = options.rushGlobalFolder.path; - const cacheFilePath: string = path.join(rushGlobalFolderPath, CACHE_FILENAME); + const rushUserFolderPath: string = RushUserConfiguration.getRushUserFolderPath(); + const cacheFilePath: string = path.join(rushUserFolderPath, CACHE_FILENAME); const jsonSchema: JsonSchema = JsonSchema.fromFile( path.resolve(__dirname, '..', 'schemas', 'credentials.schema.json') ); @@ -76,7 +75,7 @@ export class CredentialCache implements IDisposable { let lockfile: LockFile | undefined; if (options.supportEditing) { - lockfile = await LockFile.acquire(rushGlobalFolderPath, `${CACHE_FILENAME}.lock`); + lockfile = await LockFile.acquire(rushUserFolderPath, `${CACHE_FILENAME}.lock`); } const credentialCache: CredentialCache = new CredentialCache(cacheFilePath, loadedJson, lockfile); diff --git a/apps/rush-lib/src/logic/RushConstants.ts b/apps/rush-lib/src/logic/RushConstants.ts index 9850e7a1252..611630729ce 100644 --- a/apps/rush-lib/src/logic/RushConstants.ts +++ b/apps/rush-lib/src/logic/RushConstants.ts @@ -199,4 +199,9 @@ export class RushConstants { * crypto.createHash('sha1').update('a').update('bc').digest('hex') === crypto.createHash('sha1').update('ab').update('c').digest('hex') */ public static readonly hashDelimiter: string = '|'; + + /** + * The name of the per-user Rush configuration data folder. + */ + public static readonly rushUserConfigurationFolderName: string = '.rush-user'; } diff --git a/apps/rush-lib/src/logic/buildCache/AzureStorageBuildCacheProvider.ts b/apps/rush-lib/src/logic/buildCache/AzureStorageBuildCacheProvider.ts index fbf1cd45ac3..0663e819b2a 100644 --- a/apps/rush-lib/src/logic/buildCache/AzureStorageBuildCacheProvider.ts +++ b/apps/rush-lib/src/logic/buildCache/AzureStorageBuildCacheProvider.ts @@ -17,7 +17,6 @@ import { import { AzureAuthorityHosts, DeviceCodeCredential, DeviceCodeInfo } from '@azure/identity'; import { EnvironmentConfiguration, EnvironmentVariableNames } from '../../api/EnvironmentConfiguration'; -import { RushGlobalFolder } from '../../api/RushGlobalFolder'; import { CredentialCache, ICredentialCacheEntry } from '../CredentialCache'; import { URLSearchParams } from 'url'; import { RushConstants } from '../RushConstants'; @@ -31,7 +30,6 @@ export interface IAzureStorageBuildCacheProviderOptions extends IBuildCacheProvi azureEnvironment?: AzureEnvironmentNames; blobPrefix?: string; isCacheWriteAllowed: boolean; - rushGlobalFolder: RushGlobalFolder; } const SAS_TTL_MILLISECONDS: number = 7 * 24 * 60 * 60 * 1000; // Seven days @@ -42,7 +40,6 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase { private readonly _azureEnvironment: AzureEnvironmentNames; private readonly _blobPrefix: string | undefined; private readonly _isCacheWriteAllowed: boolean; - private readonly _rushGlobalFolder: RushGlobalFolder; private __credentialCacheId: string | undefined; private _containerClient: ContainerClient | undefined; @@ -54,7 +51,6 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase { this._azureEnvironment = options.azureEnvironment || 'AzurePublicCloud'; this._blobPrefix = options.blobPrefix; this._isCacheWriteAllowed = options.isCacheWriteAllowed; - this._rushGlobalFolder = options.rushGlobalFolder; if (!(this._azureEnvironment in AzureAuthorityHosts)) { throw new Error( @@ -119,7 +115,6 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase { public async updateCachedCredentialAsync(terminal: Terminal, credential: string): Promise { await CredentialCache.usingAsync( { - rushGlobalFolder: this._rushGlobalFolder, supportEditing: true }, async (credentialsCache: CredentialCache) => { @@ -135,7 +130,6 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase { await CredentialCache.usingAsync( { - rushGlobalFolder: this._rushGlobalFolder, supportEditing: true }, async (credentialsCache: CredentialCache) => { @@ -148,7 +142,6 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase { public async deleteCachedCredentialsAsync(terminal: Terminal): Promise { await CredentialCache.usingAsync( { - rushGlobalFolder: this._rushGlobalFolder, supportEditing: true }, async (credentialsCache: CredentialCache) => { @@ -171,7 +164,6 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase { let cacheEntry: ICredentialCacheEntry | undefined; await CredentialCache.usingAsync( { - rushGlobalFolder: this._rushGlobalFolder, supportEditing: false }, (credentialsCache: CredentialCache) => { diff --git a/apps/rush-lib/src/logic/buildCache/FileSystemBuildCacheProvider.ts b/apps/rush-lib/src/logic/buildCache/FileSystemBuildCacheProvider.ts index 484ad3bcac3..ec409a62f0b 100644 --- a/apps/rush-lib/src/logic/buildCache/FileSystemBuildCacheProvider.ts +++ b/apps/rush-lib/src/logic/buildCache/FileSystemBuildCacheProvider.ts @@ -5,10 +5,12 @@ import * as path from 'path'; import { AlreadyReportedError, FileSystem, Terminal } from '@rushstack/node-core-library'; import { RushConfiguration } from '../../api/RushConfiguration'; +import { RushUserConfiguration } from '../../api/RushUserConfiguration'; import { BuildCacheProviderBase, IBuildCacheProviderBaseOptions } from './BuildCacheProviderBase'; export interface IFileSystemBuildCacheProviderOptions extends IBuildCacheProviderBaseOptions { rushConfiguration: RushConfiguration; + rushUserConfiguration: RushUserConfiguration; } const BUILD_CACHE_FOLDER_NAME: string = 'build-cache'; @@ -18,7 +20,9 @@ export class FileSystemBuildCacheProvider extends BuildCacheProviderBase { public constructor(options: IFileSystemBuildCacheProviderOptions) { super(options); - this._cacheFolderPath = path.join(options.rushConfiguration.commonTempFolder, BUILD_CACHE_FOLDER_NAME); + this._cacheFolderPath = + options.rushUserConfiguration.buildCacheFolder || + path.join(options.rushConfiguration.commonTempFolder, BUILD_CACHE_FOLDER_NAME); } public async tryGetCacheEntryBufferByIdAsync( diff --git a/apps/rush-lib/src/logic/buildCache/test/AzureStorageBuildCacheProvider.test.ts b/apps/rush-lib/src/logic/buildCache/test/AzureStorageBuildCacheProvider.test.ts index 5e85ebd17fa..2cad46f4b1b 100644 --- a/apps/rush-lib/src/logic/buildCache/test/AzureStorageBuildCacheProvider.test.ts +++ b/apps/rush-lib/src/logic/buildCache/test/AzureStorageBuildCacheProvider.test.ts @@ -11,8 +11,7 @@ describe('AzureStorageBuildCacheProvider', () => { storageAccountName: 'storage-account', storageContainerName: 'container-name', azureEnvironment: 'INCORRECT_AZURE_ENVIRONMENT' as AzureEnvironmentNames, - isCacheWriteAllowed: false, - rushGlobalFolder: undefined! + isCacheWriteAllowed: false }) ).toThrowErrorMatchingSnapshot(); }); diff --git a/apps/rush-lib/src/schemas/rush-user-settings.schema.json b/apps/rush-lib/src/schemas/rush-user-settings.schema.json new file mode 100644 index 00000000000..dde6f7e06f3 --- /dev/null +++ b/apps/rush-lib/src/schemas/rush-user-settings.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rush per-user settings file", + "description": "For use with the Rush tool, this file stores user-specific settings options. See http://rushjs.io for details.", + + "type": "object", + "properties": { + "$schema": { + "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", + "type": "string" + }, + + "buildCacheFolder": { + "type": "string", + "description": "If provided, store build cache in the specified folder. Must be an absolute path." + } + }, + "additionalProperties": false +} diff --git a/apps/rush-lib/src/utilities/Utilities.ts b/apps/rush-lib/src/utilities/Utilities.ts index c2d94e39697..2972940dd7c 100644 --- a/apps/rush-lib/src/utilities/Utilities.ts +++ b/apps/rush-lib/src/utilities/Utilities.ts @@ -122,7 +122,7 @@ export class Utilities { * Get the user's home directory. On windows this looks something like "C:\users\username\" and on UNIX * this looks something like "/home/username/" */ - public static getHomeDirectory(): string { + public static getHomeFolder(): string { const unresolvedUserFolder: string | undefined = process.env[process.platform === 'win32' ? 'USERPROFILE' : 'HOME']; const dirError: string = "Unable to determine the current user's home directory"; diff --git a/common/changes/@microsoft/rush/ianc-user-config-file_2020-12-22-07-45.json b/common/changes/@microsoft/rush/ianc-user-config-file_2020-12-22-07-45.json new file mode 100644 index 00000000000..8b978fe48f3 --- /dev/null +++ b/common/changes/@microsoft/rush/ianc-user-config-file_2020-12-22-07-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "", + "type": "none" + } + ], + "packageName": "@microsoft/rush", + "email": "iclanton@users.noreply.github.com" +} \ No newline at end of file