-
Notifications
You must be signed in to change notification settings - Fork 81
feat: Clean cache command #1394
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
base: main
Are you sure you want to change the base?
Changes from all commits
8280272
1971750
637f2b5
d302ef6
0c16994
b0c3f3b
21e2e89
329ba63
884d024
046c401
b53c357
81c130a
21b5277
406058d
5b099ce
2f4e7a0
6b92cbd
9558e61
cfde118
086c9e3
a51c16d
6fa610a
4ee4e5f
5c2cac8
5045721
b38256f
132a7e2
84d590b
73cd772
8c73b1e
6c9872d
0501f1b
cb1f1d4
6f02c79
6391071
016bd18
137360d
f5e5d86
b7817b1
970156f
de109a2
9fce7eb
c2db60e
d258614
4e36bd0
2c534e3
40a861e
bbecbd6
937acfb
596f898
873712e
486340b
9b333b9
6d87bf6
0d446a6
49c560d
40c19a4
c8b4484
e71b97d
df56779
abeb88d
6b80d8b
2276a08
07cffbc
5cac4c7
98389e5
a31c263
0630e18
747e1e0
db6b3de
0491e72
630324f
63bd310
807a7b2
5595e41
82f5009
0d0f10a
a2dda60
11c7157
cde5eb4
c8bb468
adca562
eb4c950
ea52640
cb4c899
b0d1d94
3097cdf
3d51af4
3fb827c
5fc6c4c
87ea214
4ecb384
c9e191e
4778dbb
5cbce64
31ab373
d0d3c46
3f5c3d3
06aca52
84a9d5e
36b4062
10c0334
24866d9
d733088
6319d07
afa9f69
8002c04
1a67819
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -26,22 +26,27 @@ Only remove these directories when no UI5 CLI process and no `@ui5/*` API consum | |||||
|
|
||||||
| #### Resolution | ||||||
|
|
||||||
| To free disk space, remove the relevant subdirectory. | ||||||
|
|
||||||
| To only remove framework downloads: | ||||||
| Use the dedicated cache clean command, which safely removes all cached data: | ||||||
|
|
||||||
| ```sh | ||||||
| rm -rf ~/.ui5/framework/ | ||||||
| ui5 cache clean | ||||||
| ``` | ||||||
|
|
||||||
| To only remove the build cache: | ||||||
| This displays the cache location, the amount of data that gets removed, and asks for confirmation before proceeding. To skip the confirmation prompt (for example in CI environments), use the `--yes` flag: | ||||||
|
|
||||||
| ```sh | ||||||
| rm -rf ~/.ui5/buildCache/ | ||||||
| ui5 cache clean --yes | ||||||
| ``` | ||||||
|
|
||||||
| The command removes the following cached data: | ||||||
| - **UI5 framework packages** — downloaded UI5 library files (`~/.ui5/framework/`) | ||||||
| - **Build cache (DB)** — build data (`~/.ui5/buildCache/`) | ||||||
| - **Orphaned framework data** — incomplete framework directories left over from previously interrupted cleanup operations (`~/.ui5/.framework_to_delete_*/`) | ||||||
|
|
||||||
| Any missing framework dependencies are downloaded during the next UI5 CLI invocation. | ||||||
|
|
||||||
| ::: info | ||||||
| If you have configured a custom data directory via `UI5_DATA_DIR` or `ui5 config set ui5DataDir`, replace `~/.ui5/` with that path. See [Changing UI5 CLI's Data Directory](#changing-ui5-cli-s-data-directory). | ||||||
| If you have configured a custom data directory via `UI5_DATA_DIR` or `ui5 config set ui5DataDir`, the `ui5 cache clean` command automatically cleans up that location instead of the default `~/.ui5/`. See [Changing UI5 CLI's Data Directory](#changing-ui5-cli-s-data-directory). | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ::: | ||||||
|
|
||||||
| ## Environment Variables | ||||||
|
|
||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,324 @@ | ||
| import chalk from "chalk"; | ||
| import path from "node:path"; | ||
| import process from "node:process"; | ||
| import baseMiddleware from "../middlewares/base.js"; | ||
| import {resolveUi5DataDir} from "@ui5/project/utils/dataDir"; | ||
| import {acquireLock, CLEANUP_LOCK_NAME, getLockDir, hasActiveLocks} from "@ui5/project/utils/lock"; | ||
| import * as frameworkCache from "@ui5/project/ui5Framework/cache"; | ||
| import CacheManager from "@ui5/project/build/cache/CacheManager"; | ||
|
|
||
| const cacheCommand = { | ||
| command: "cache", | ||
| describe: "Manage the UI5 CLI cache (downloaded framework packages and build data)", | ||
| middlewares: [baseMiddleware], | ||
| handler: handleCache | ||
| }; | ||
|
|
||
| cacheCommand.builder = function(cli) { | ||
| return cli | ||
| .demandCommand(1, "Command required. Available command is 'clean'") | ||
| .command("clean", "Remove all cached UI5 data", { | ||
|
d3xter666 marked this conversation as resolved.
|
||
| handler: handleCache, | ||
| builder: function(yargs) { | ||
| return yargs | ||
| .option("yes", { | ||
| alias: "y", | ||
| describe: "Skip the confirmation prompt, e.g. for use in CI pipelines", | ||
| default: false, | ||
| type: "boolean", | ||
| }) | ||
| .example("$0 cache clean", | ||
| "Remove all cached UI5 data after confirmation") | ||
| .example("$0 cache clean --yes", | ||
| "Remove all cached UI5 data without confirmation (e.g. in CI scenarios)") | ||
| .example("UI5_DATA_DIR=/custom/path $0 cache clean", | ||
| "Remove cached data from a non-default UI5 data directory") | ||
| .epilogue( | ||
| "The cache is stored in the UI5 data directory (default: ~/.ui5).\n" + | ||
| "Override the location with the UI5_DATA_DIR environment variable or\n" + | ||
| "the 'ui5 config set ui5DataDir' configuration option (see 'ui5 config --help').\n\n" + | ||
| "The following cache types are removed:\n" + | ||
| " UI5 framework packages: Downloaded UI5 library files " + | ||
| "(~/.ui5/framework/)\n" + | ||
| " Build cache (DB): Build data " + | ||
| "(~/.ui5/buildCache/)\n" + | ||
| " Orphaned framework data: Incomplete directories from previously interrupted cleanups\n" + | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: Is this relevant enough to mention it in the user facing docs? |
||
| " (~/.ui5/.framework_to_delete_*/)" | ||
| ); | ||
| }, | ||
| middlewares: [baseMiddleware], | ||
| }); | ||
| }; | ||
|
|
||
| const LABEL_FRAMEWORK = "UI5 Framework packages"; | ||
| const LABEL_BUILD = "Build cache (DB)"; | ||
| // Pad labels to equal width for two-column alignment | ||
| const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length); | ||
|
|
||
| /** | ||
| * Format a byte size as a human-readable string. | ||
| * | ||
| * @param {number} bytes Size in bytes | ||
| * @returns {string} Formatted size string | ||
| */ | ||
| function formatSize(bytes) { | ||
| if (bytes < 1024) { | ||
| return `${bytes} B`; | ||
| } else if (bytes < 1024 * 1024) { | ||
| return `${(bytes / 1024).toFixed(1)} KB`; | ||
| } else if (bytes < 1024 * 1024 * 1024) { | ||
| return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; | ||
| } | ||
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; | ||
| } | ||
|
|
||
| /** | ||
| * Format framework cache stats as a human-readable detail string. | ||
| * E.g. "1,189 versions of 155 libraries" or "1 version of 1 library". | ||
| * | ||
| * @param {number} libraryCount | ||
| * @param {number} versionCount | ||
| * @returns {string} | ||
| */ | ||
| function formatFrameworkStats(libraryCount, versionCount) { | ||
| const v = `${versionCount.toLocaleString("en-US")} ${versionCount === 1 ? "version" : "versions"}`; | ||
| const l = `${libraryCount.toLocaleString("en-US")} ${libraryCount === 1 ? "library" : "libraries"}`; | ||
| return `${v} of ${l}`; | ||
| } | ||
|
|
||
| /** | ||
| * Pad a label to the shared column width. | ||
| * | ||
| * @param {string} label | ||
| * @returns {string} | ||
| */ | ||
| function padLabel(label) { | ||
| return label.padEnd(LABEL_WIDTH); | ||
| } | ||
|
|
||
| /** | ||
| * Display information about the cached data that will be removed, | ||
| * including the absolute paths and details about the framework and build caches, | ||
| * and any orphaned staging directories from previously interrupted clean operations. | ||
| * | ||
| * @param {object} data | ||
| * @param {object|null} data.frameworkInfo | ||
| * @param {object|null} data.buildInfo | ||
| * @param {string|null} data.frameworkAbsPath | ||
| * @param {string|null} data.buildAbsPath | ||
| * @param {number} data.buildPreSize | ||
| * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfo | ||
| */ | ||
| async function displayCacheInfo({ | ||
| frameworkInfo, | ||
| buildInfo, | ||
| frameworkAbsPath, | ||
| buildAbsPath, | ||
| buildPreSize, | ||
| orphanedInfo, | ||
| }) { | ||
| // Display items that will be removed | ||
| process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n")); | ||
| if (frameworkInfo) { | ||
| const detail = formatFrameworkStats(frameworkInfo.libraryCount, frameworkInfo.versionCount); | ||
| process.stderr.write( | ||
| ` ${chalk.yellow("•")} ${padLabel(LABEL_FRAMEWORK)} ${frameworkAbsPath} (${detail})\n` | ||
| ); | ||
| } | ||
| if (buildInfo) { | ||
| const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; | ||
| process.stderr.write( | ||
| ` ${chalk.yellow("•")} ${padLabel(LABEL_BUILD)} ${buildAbsPath} (${detail})\n` | ||
| ); | ||
| } | ||
| if (orphanedInfo && orphanedInfo.length > 0) { | ||
| process.stderr.write( | ||
| ` ${chalk.yellow("•")} ${chalk.bold("Orphaned framework data")}` + | ||
| ` (incomplete previous clean — ` + | ||
| `${orphanedInfo.length} director${orphanedInfo.length === 1 ? "y" : "ies"})\n` | ||
| ); | ||
| for (const orphan of orphanedInfo) { | ||
| const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); | ||
| process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); | ||
| } | ||
| } | ||
| process.stderr.write("\n"); | ||
| } | ||
|
|
||
| /** | ||
| * Display the result of the cache cleanup operation, | ||
| * including which caches were removed and their details, | ||
| * and any orphaned staging directories that were also cleaned up. | ||
| * | ||
| * @param {object} data | ||
| * @param {object|null} data.frameworkResult | ||
| * @param {object|null} data.buildResult | ||
| * @param {string|null} data.frameworkAbsPath | ||
| * @param {string|null} data.buildAbsPath | ||
| * @param {number} data.buildPreSize | ||
| * @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfoWithAbsPaths | ||
| */ | ||
| async function displayCleanupResult({ | ||
| frameworkResult, | ||
| buildResult, | ||
| frameworkAbsPath, | ||
| buildAbsPath, | ||
| buildPreSize, | ||
| orphanedInfoWithAbsPaths, | ||
| }) { | ||
| process.stderr.write("\n"); | ||
| if (frameworkResult && frameworkAbsPath) { | ||
| const detail = formatFrameworkStats( | ||
| frameworkResult.libraryCount, | ||
| frameworkResult.versionCount, | ||
| ); | ||
| process.stderr.write( | ||
| `${chalk.green("✓")} Removed ${chalk.bold(LABEL_FRAMEWORK)}` + | ||
| ` (${frameworkAbsPath} · ${detail})\n`, | ||
| ); | ||
| } | ||
| if (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) { | ||
| process.stderr.write( | ||
| `${chalk.green("✓")} Removed ${chalk.bold("Orphaned framework data")}` + | ||
| ` (${orphanedInfoWithAbsPaths.length}` + | ||
| ` director${orphanedInfoWithAbsPaths.length === 1 ? "y" : "ies"})\n` | ||
| ); | ||
| for (const orphan of orphanedInfoWithAbsPaths) { | ||
| const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount); | ||
| process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`); | ||
| } | ||
| } | ||
| if (buildResult) { | ||
| // Use pre-clean size so the number matches what was shown before confirmation | ||
| const detail = buildPreSize > 0 ? formatSize(buildPreSize) : ""; | ||
| process.stderr.write( | ||
| `${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` + | ||
| ` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n`, | ||
| ); | ||
| } | ||
|
|
||
| // Success summary | ||
| const cleaned = []; | ||
| if (frameworkResult) { | ||
| cleaned.push(LABEL_FRAMEWORK); | ||
| } | ||
| if (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) { | ||
| cleaned.push("Orphaned framework data"); | ||
| } | ||
| if (buildResult) { | ||
| cleaned.push(LABEL_BUILD); | ||
| } | ||
| process.stderr.write( | ||
| `\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Prompt the user for confirmation before proceeding with cache cleanup. | ||
| * | ||
| * @param {Yargs.Arguments} argv | ||
| * @returns {Promise<boolean>} Confirmation result | ||
| */ | ||
| async function getConfirmation(argv) { | ||
| if (argv.yes) { | ||
| return true; | ||
| } | ||
| const {default: yesno} = await import("yesno"); | ||
| return yesno({ | ||
| question: "Do you want to continue? (y/N)", | ||
| defaultValue: false | ||
| }); | ||
| } | ||
|
|
||
| async function handleCache(argv) { | ||
| const ui5DataDir = await resolveUi5DataDir(); | ||
| const lockDir = getLockDir(ui5DataDir); | ||
| const lockPath = path.join(lockDir, CLEANUP_LOCK_NAME); | ||
|
|
||
| // Acquire first, then check — ensures concurrent framework operations will see | ||
| // the cleanup lock and abort before the actual cleanup. | ||
| const releaseCleanupLock = await acquireLock(lockPath); | ||
|
|
||
| try { | ||
| // Abort early if a lock is active — before prompting the user. | ||
| if (await hasActiveLocks(ui5DataDir, {exclude: CLEANUP_LOCK_NAME})) { | ||
| process.stderr.write( | ||
| `${chalk.red("Error:")} A UI5 server or build process is currently running. ` + | ||
| "Cannot clean the cache while it is in use. " + | ||
| "Please stop all running 'ui5 serve' or wait for 'ui5 build' processes to finish.\n" | ||
| ); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`); | ||
|
|
||
| const [frameworkInfo, buildInfo, orphanedInfo] = await Promise.all([ | ||
| frameworkCache.getCacheInfo(ui5DataDir), | ||
| CacheManager.getCacheInfo(ui5DataDir), | ||
| frameworkCache.getOrphanedInfo(ui5DataDir), | ||
| ]); | ||
|
|
||
| if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0) { | ||
| process.stderr.write("Nothing to clean\n"); | ||
| return; | ||
| } | ||
|
|
||
| // Compute absolute paths once — producers return relative sub-path segments | ||
| const frameworkAbsPath = frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null; | ||
| const buildAbsPath = buildInfo ? path.join(ui5DataDir, buildInfo.path) : null; | ||
| const buildPreSize = buildInfo?.size ?? 0; | ||
| const preCleanOrphanedInfo = orphanedInfo.map( | ||
| (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) | ||
| ); | ||
|
|
||
| await displayCacheInfo({ | ||
| frameworkInfo, | ||
| buildInfo, | ||
| frameworkAbsPath, | ||
| buildAbsPath, | ||
| buildPreSize, | ||
| orphanedInfo: preCleanOrphanedInfo, | ||
| }); | ||
|
|
||
| const confirmed = await getConfirmation(argv); | ||
| if (!confirmed) { | ||
| process.stderr.write("Cancelled\n"); | ||
| return; | ||
| } | ||
|
|
||
| const [frameworkResult, buildResult] = await Promise.all([ | ||
| frameworkCache.cleanCache(ui5DataDir), | ||
| CacheManager.cleanCache(ui5DataDir), | ||
| ]); | ||
|
|
||
| // Release the lock. Critical sections are done. | ||
| // The finally block will call releaseCleanupLock() again, which is a no-op (idempotent). | ||
| releaseCleanupLock(); | ||
|
|
||
| // Clean additional resources that are safe to run outside the lock. | ||
| // For the framework cache this handles orphaned staging dirs from previous | ||
| // interrupted cleans. These are fully independent of any active operation. | ||
| const [additionalFrameworkResult] = await Promise.all([ | ||
| frameworkCache.cleanAdditional(ui5DataDir), | ||
| // The same interface. No-op | ||
| CacheManager.cleanAdditional(ui5DataDir), | ||
| ]); | ||
|
Comment on lines
+290
to
+306
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the reason for splitting the cache clean into two operations? Can't we just keep the lock and clean "additional" resources in the same method? |
||
| const orphanedInfoWithAbsPaths = additionalFrameworkResult.map( | ||
| (o) => ({...o, absPath: path.join(ui5DataDir, o.path)}) | ||
| ); | ||
|
|
||
| await displayCleanupResult({ | ||
| frameworkResult, | ||
| buildResult, | ||
| frameworkAbsPath, | ||
| buildAbsPath, | ||
| buildPreSize, | ||
| orphanedInfoWithAbsPaths, | ||
| }); | ||
| } finally { | ||
| releaseCleanupLock(); | ||
| } | ||
| } | ||
|
|
||
| export default cacheCommand; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.