Skip to content

Commit 2c1f883

Browse files
author
Ben Keen
committed
Add bridge-cache plugin to populate cache without running action
1 parent 78ff506 commit 2c1f883

File tree

21 files changed

+322
-9
lines changed

21 files changed

+322
-9
lines changed

apps/rush/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@rushstack/rush-amazon-s3-build-cache-plugin": "workspace:*",
4848
"@rushstack/rush-azure-storage-build-cache-plugin": "workspace:*",
4949
"@rushstack/rush-http-build-cache-plugin": "workspace:*",
50+
"@rushstack/rush-bridge-cache-plugin": "workspace:*",
5051
"@types/heft-jest": "1.0.1",
5152
"@types/semver": "7.5.0"
5253
}

apps/rush/src/start-dev-docs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ import { Colorize, ConsoleTerminalProvider, Terminal } from '@rushstack/terminal
66
const terminal: Terminal = new Terminal(new ConsoleTerminalProvider());
77

88
terminal.writeLine('For instructions on debugging Rush, please see this documentation:');
9-
terminal.writeLine(Colorize.bold('https://rushjs.io/pages/contributing/debugging/'));
9+
terminal.writeLine(Colorize.bold('https://rushjs.io/pages/contributing/#debugging-rush'));

apps/rush/src/start-dev.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ includePlugin('rush-azure-storage-build-cache-plugin');
3131
includePlugin('rush-http-build-cache-plugin');
3232
// Including this here so that developers can reuse it without installing the plugin a second time
3333
includePlugin('rush-azure-interactive-auth-plugin', '@rushstack/rush-azure-storage-build-cache-plugin');
34+
includePlugin('rush-bridge-cache-plugin');
3435

3536
const currentPackageVersion: string = PackageJsonLookup.loadOwnPackageJson(__dirname).version;
3637
RushCommandSelector.execute(currentPackageVersion, rushLib, {

common/config/rush/browser-approved-packages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
"name": "@reduxjs/toolkit",
3939
"allowedCategories": [ "libraries", "vscode-extensions" ]
4040
},
41+
{
42+
"name": "@rushstack/rush-bridge-cache-plugin",
43+
"allowedCategories": [ "libraries" ]
44+
},
4145
{
4246
"name": "@rushstack/rush-themed-ui",
4347
"allowedCategories": [ "libraries" ]

common/config/subspaces/default/pnpm-lock.yaml

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/reviews/api/rush-lib.api.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { AsyncParallelHook } from 'tapable';
1010
import { AsyncSeriesBailHook } from 'tapable';
1111
import { AsyncSeriesHook } from 'tapable';
1212
import { AsyncSeriesWaterfallHook } from 'tapable';
13-
import type { CollatedWriter } from '@rushstack/stream-collator';
13+
import { CollatedWriter } from '@rushstack/stream-collator';
1414
import type { CommandLineParameter } from '@rushstack/ts-command-line';
1515
import { CommandLineParameterKind } from '@rushstack/ts-command-line';
1616
import { HookMap } from 'tapable';
@@ -23,7 +23,8 @@ import { JsonNull } from '@rushstack/node-core-library';
2323
import { JsonObject } from '@rushstack/node-core-library';
2424
import { LookupByPath } from '@rushstack/lookup-by-path';
2525
import { PackageNameParser } from '@rushstack/node-core-library';
26-
import type { StdioSummarizer } from '@rushstack/terminal';
26+
import { StdioSummarizer } from '@rushstack/terminal';
27+
import { StreamCollator } from '@rushstack/stream-collator';
2728
import { SyncHook } from 'tapable';
2829
import { SyncWaterfallHook } from 'tapable';
2930
import { Terminal } from '@rushstack/terminal';
@@ -1127,6 +1128,27 @@ export type PnpmStoreLocation = 'local' | 'global';
11271128
// @public @deprecated (undocumented)
11281129
export type PnpmStoreOptions = PnpmStoreLocation;
11291130

1131+
// Warning: (ae-internal-missing-underscore) The name "ProjectBuildCache" should be prefixed with an underscore because the declaration is marked as @internal
1132+
//
1133+
// @internal (undocumented)
1134+
export class ProjectBuildCache {
1135+
// (undocumented)
1136+
get cacheId(): string | undefined;
1137+
// Warning: (ae-forgotten-export) The symbol "OperationExecutionRecord" needs to be exported by the entry point index.d.ts
1138+
// Warning: (ae-forgotten-export) The symbol "IOperationBuildCacheOptions" needs to be exported by the entry point index.d.ts
1139+
//
1140+
// (undocumented)
1141+
static forOperation(operation: OperationExecutionRecord, options: IOperationBuildCacheOptions): ProjectBuildCache;
1142+
// Warning: (ae-forgotten-export) The symbol "IProjectBuildCacheOptions" needs to be exported by the entry point index.d.ts
1143+
//
1144+
// (undocumented)
1145+
static getProjectBuildCache(options: IProjectBuildCacheOptions): ProjectBuildCache;
1146+
// (undocumented)
1147+
tryRestoreFromCacheAsync(terminal: ITerminal, specifiedCacheId?: string): Promise<boolean>;
1148+
// (undocumented)
1149+
trySetCacheEntryAsync(terminal: ITerminal, specifiedCacheId?: string): Promise<boolean>;
1150+
}
1151+
11301152
// @beta (undocumented)
11311153
export class ProjectChangeAnalyzer {
11321154
constructor(rushConfiguration: RushConfiguration);

libraries/rush-lib/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,5 @@ export {
197197
type IRushCommandLineParameter,
198198
type IRushCommandLineAction
199199
} from './api/RushCommandLine';
200+
201+
export { ProjectBuildCache } from './logic/buildCache/ProjectBuildCache';

libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ interface IPathsToCache {
5050
outputFilePaths: string[];
5151
}
5252

53+
/**
54+
* @internal
55+
*/
5356
export class ProjectBuildCache {
5457
private static _tarUtilityPromise: Promise<TarExecutable | undefined> | undefined;
5558

libraries/rush-lib/src/pluginFramework/PluginManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export class PluginManager {
8181
tryAddBuiltInPlugin('rush-amazon-s3-build-cache-plugin');
8282
tryAddBuiltInPlugin('rush-azure-storage-build-cache-plugin');
8383
tryAddBuiltInPlugin('rush-http-build-cache-plugin');
84+
tryAddBuiltInPlugin('rush-bridge-cache-plugin');
8485
// This is a secondary plugin inside the `@rushstack/rush-azure-storage-build-cache-plugin`
8586
// package. Because that package comes with Rush (for now), it needs to get registered here.
8687
// If the necessary config file doesn't exist, this plugin doesn't do anything.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// This is a workaround for https://github.com/eslint/eslint/issues/3458
2+
require('local-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution');
3+
// This is a workaround for https://github.com/microsoft/rushstack/issues/3021
4+
require('local-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names');
5+
6+
module.exports = {
7+
extends: [
8+
'local-node-rig/profiles/default/includes/eslint/profile/node',
9+
'local-node-rig/profiles/default/includes/eslint/mixins/friendly-locals',
10+
'local-node-rig/profiles/default/includes/eslint/mixins/tsdoc'
11+
],
12+
parserOptions: { tsconfigRootDir: __dirname }
13+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@rushstack/rush-serve-plugin
2+
3+
Copyright (c) Microsoft Corporation. All rights reserved.
4+
5+
MIT License
6+
7+
Permission is hereby granted, free of charge, to any person obtaining
8+
a copy of this software and associated documentation files (the
9+
"Software"), to deal in the Software without restriction, including
10+
without limitation the rights to use, copy, modify, merge, publish,
11+
distribute, sublicense, and/or sell copies of the Software, and to
12+
permit persons to whom the Software is furnished to do so, subject to
13+
the following conditions:
14+
15+
The above copyright notice and this permission notice shall be
16+
included in all copies or substantial portions of the Software.
17+
18+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# @rushstack/rush-bridge-cache-plugin
2+
3+
This plugin allows for interaction with the Rush cache. It exposes some methods to set the cache.
4+
5+
6+
## Installation
7+
8+
`npm install @rushstack/rush-bridge-cache-plugin`
9+
10+
<!--
11+
## Usage
12+
The package contains a binary you can run from the command line. It will auto-detect the location of your rush.json file.
13+
14+
`populate-rush-cache --packageName=[package name] --phase=test:unit`
15+
16+
- `--packageName` (required) - this should map to the `packageName` entry in your `rush.json` file.
17+
- `--phase` (required): the name of the phased command whose command has already been ran, and you want to cache the result on disk.
18+
-->
19+
20+
--------------------
21+
22+
So this will:
23+
- tap into the hooks and do all the necessary shit to figure out how to po
24+
- expose a simple API for external users to tap into, e.g. if you want to populate the cache externally you'd import this plugin and use the methods to do so. You could then wrap that in a rush function, or however you want to do it.
25+
26+
27+
28+
29+
30+
--------------------
31+
32+
Discussion about the solution for BuildXL here:
33+
https://teams.microsoft.com/l/message/19:d85f52548ec74e8f8a0f107bd4e5ceb6@thread.v2/1740610046597?context=%7B%22contextType%22%3A%22chat%22%7D
34+
35+
36+
"populate-cache": "..." <--- called after any cacheable unit of work is complete
37+
38+
OperationExecutionRecord -> the smallest unit of work to be done.
39+
- looks like it's strongly coupled to the runner. We need one-off method calls.
40+
41+
CacheableOperationPlugin
42+
_tryGetProjectBuildCache() -> this returns the project build cache
43+
44+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json",
3+
"parameters": [
4+
{
5+
"longName": "--set-cache-only",
6+
"parameterKind": "flag",
7+
"description": "...",
8+
"associatedCommands": ["build"] // has to either apply to all phased command, or allow customization by user
9+
}
10+
]
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "local-node-rig/profiles/default/config/jest.config.json"
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
// The "rig.json" file directs tools to look for their config files in an external package.
3+
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
4+
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
5+
6+
"rigPackageName": "local-node-rig"
7+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@rushstack/rush-bridge-cache-plugin",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "A plugin to expose methods to interact with the Rush cache.",
6+
"license": "MIT",
7+
"main": "./lib/index.js",
8+
"repository": {
9+
"url": "https://github.com/microsoft/rushstack.git",
10+
"type": "git",
11+
"directory": "rush-plugins/rush-bridge-cache-plugin"
12+
},
13+
"scripts": {
14+
"build": "heft test --clean",
15+
"_phase:build": "heft run --only build -- --clean",
16+
"_phase:test": "heft run --only test -- --clean"
17+
},
18+
"devDependencies": {
19+
"@rushstack/node-core-library": "workspace:*",
20+
"@microsoft/rush-lib": "workspace:*",
21+
"@rushstack/rush-sdk": "workspace:*",
22+
"@rushstack/terminal": "workspace:*",
23+
"@rushstack/heft": "workspace:*",
24+
"local-node-rig": "workspace:*"
25+
}
26+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugin-manifest.schema.json",
3+
"plugins": [
4+
{
5+
"pluginName": "rush-bridge-cache-plugin",
6+
"description": "Rush plugin to allow interactions with the Rush cache.",
7+
"entryPoint": "./lib/index.js",
8+
"commandLineJsonFilePath": "./command-line.json"
9+
}
10+
]
11+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import { BuildCacheConfiguration, ProjectBuildCache } from '@rushstack/rush-sdk';
5+
import type {
6+
ILogger,
7+
IOperationExecutionResult,
8+
IPhasedCommand,
9+
IRushPlugin,
10+
Operation,
11+
RushConfiguration,
12+
RushSession
13+
} from '@rushstack/rush-sdk';
14+
15+
const PLUGIN_NAME: 'RushBridgeCachePlugin' = 'RushBridgeCachePlugin';
16+
17+
export class BridgeCachePlugin implements IRushPlugin {
18+
public readonly pluginName: string = PLUGIN_NAME;
19+
20+
public apply(session: RushSession, rushConfiguration: RushConfiguration): void {
21+
// klutzy. Better way?
22+
const actionName: string = process.argv[2];
23+
24+
// const isSetCacheOnly: boolean = process.argv.includes('--set-cache-only'); // has to be allowed for ANY phased command, or be customizable
25+
// if (!isSetCacheOnly) {
26+
// return;
27+
// }
28+
29+
// tracks the projects being targeted by the command (--to, --only etc.)
30+
const targetProjects: string[] = [];
31+
32+
const cancelOperations = (operations: Set<Operation>): Set<Operation> => {
33+
operations.forEach((operation: Operation) => {
34+
if (operation.enabled) {
35+
targetProjects.push(operation.associatedProject.packageName);
36+
}
37+
38+
operation.enabled = false;
39+
});
40+
return operations;
41+
};
42+
43+
session.hooks.runPhasedCommand
44+
.for(actionName)
45+
.tapPromise(PLUGIN_NAME, async (command: IPhasedCommand) => {
46+
// cancel the actual operations. We don't want to actually run the command, just cache the output folders from a previous run
47+
command.hooks.createOperations.tap(
48+
{ name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER },
49+
cancelOperations
50+
);
51+
52+
// now populate the cache for each operation
53+
command.hooks.beforeExecuteOperations.tap(
54+
PLUGIN_NAME,
55+
async (recordByOperation: Map<Operation, IOperationExecutionResult>): Promise<void> => {
56+
await this._setCacheAsync(session, rushConfiguration, recordByOperation, targetProjects);
57+
}
58+
);
59+
});
60+
}
61+
62+
private async _setCacheAsync(
63+
session: RushSession,
64+
rushConfiguration: RushConfiguration,
65+
recordByOperation: Map<Operation, IOperationExecutionResult>,
66+
targetProjects: string[]
67+
): Promise<void> {
68+
const logger: ILogger = session.getLogger(PLUGIN_NAME);
69+
70+
// const isSetCacheOnly: boolean = process.argv.includes('--set-cache-only');
71+
72+
recordByOperation.forEach(
73+
async (operationExecutionResult: IOperationExecutionResult, operation: Operation) => {
74+
const { associatedProject, associatedPhase, settings } = operation;
75+
76+
if (!targetProjects.includes(associatedProject.packageName)) {
77+
return;
78+
}
79+
80+
const buildCacheConfiguration: BuildCacheConfiguration | undefined =
81+
await BuildCacheConfiguration.tryLoadAsync(logger.terminal, rushConfiguration, session);
82+
83+
if (!buildCacheConfiguration) {
84+
return;
85+
}
86+
87+
const projectBuildCache: ProjectBuildCache = ProjectBuildCache.getProjectBuildCache({
88+
project: associatedProject,
89+
projectOutputFolderNames: settings?.outputFolderNames || [],
90+
buildCacheConfiguration,
91+
terminal: logger.terminal,
92+
operationStateHash: operationExecutionResult.getStateHash(),
93+
phaseName: associatedPhase.name
94+
});
95+
96+
const success: boolean = await projectBuildCache.trySetCacheEntryAsync(logger.terminal);
97+
98+
// eslint-disable-next-line no-console
99+
console.log('- setting cache for', {
100+
success,
101+
name: associatedPhase.name,
102+
package: associatedProject.packageName
103+
});
104+
}
105+
);
106+
}
107+
}

0 commit comments

Comments
 (0)