diff --git a/sources/src/caching/cache-cleaner.ts b/sources/src/caching/cache-cleaner.ts index 8081113c..20f3e0bc 100644 --- a/sources/src/caching/cache-cleaner.ts +++ b/sources/src/caching/cache-cleaner.ts @@ -1,5 +1,4 @@ import * as core from '@actions/core' -import * as glob from '@actions/glob' import fs from 'fs' import path from 'path' import {provisionAndMaybeExecute} from '../execution/gradle' @@ -13,25 +12,20 @@ export class CacheCleaner { this.tmpDir = tmpDir } - async prepare(): Promise { - // Reset the file-access journal so that files appear not to have been used recently - fs.rmSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true, force: true}) - fs.mkdirSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true}) - fs.writeFileSync( - path.resolve(this.gradleUserHome, 'caches/journal-1/file-access.properties'), - 'inceptionTimestamp=0' - ) - - // Set the modification time of all files to the past: this timestamp is used when there is no matching entry in the journal - await this.ageAllFiles() - - // Touch all 'gc' files so that cache cleanup won't run immediately. - await this.touchAllFiles('gc.properties') + async prepare(): Promise { + // Save the current timestamp + const timestamp = Date.now().toString() + core.saveState('clean-timestamp', timestamp) + return timestamp } async forceCleanup(): Promise { - // Age all 'gc' files so that cache cleanup will run immediately. - await this.ageAllFiles('gc.properties') + const cleanTimestamp = core.getState('clean-timestamp') + await this.forceCleanupFilesOlderThan(cleanTimestamp) + } + + async forceCleanupFilesOlderThan(cleanTimestamp: string): Promise { + core.info(`Cleaning up caches before ${cleanTimestamp}`) // Run a dummy Gradle build to trigger cache cleanup const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project') @@ -40,11 +34,31 @@ export class CacheCleaner { path.resolve(cleanupProjectDir, 'settings.gradle'), 'rootProject.name = "dummy-cleanup-project"' ) + fs.writeFileSync( + path.resolve(cleanupProjectDir, 'init.gradle'), + ` + beforeSettings { settings -> + def cleanupTime = ${cleanTimestamp} + + settings.caches { + cleanup = Cleanup.ALWAYS + + releasedWrappers.removeUnusedEntriesOlderThan.set(cleanupTime) + snapshotWrappers.removeUnusedEntriesOlderThan.set(cleanupTime) + downloadedResources.removeUnusedEntriesOlderThan.set(cleanupTime) + createdResources.removeUnusedEntriesOlderThan.set(cleanupTime) + buildCache.removeUnusedEntriesOlderThan.set(cleanupTime) + } + } + ` + ) fs.writeFileSync(path.resolve(cleanupProjectDir, 'build.gradle'), 'task("noop") {}') await provisionAndMaybeExecute('current', cleanupProjectDir, [ '-g', this.gradleUserHome, + '-I', + 'init.gradle', '--quiet', '--no-daemon', '--no-scan', @@ -53,23 +67,4 @@ export class CacheCleaner { 'noop' ]) } - - private async ageAllFiles(fileName = '*'): Promise { - core.debug(`Aging all files in Gradle User Home with name ${fileName}`) - await this.setUtimes(`${this.gradleUserHome}/**/${fileName}`, new Date(0)) - } - - private async touchAllFiles(fileName = '*'): Promise { - core.debug(`Touching all files in Gradle User Home with name ${fileName}`) - await this.setUtimes(`${this.gradleUserHome}/**/${fileName}`, new Date()) - } - - private async setUtimes(pattern: string, timestamp: Date): Promise { - const globber = await glob.create(pattern, { - implicitDescendants: false - }) - for await (const file of globber.globGenerator()) { - fs.utimesSync(file, timestamp, timestamp) - } - } } diff --git a/sources/test/jest/cache-cleanup.test.ts b/sources/test/jest/cache-cleanup.test.ts index f97dbd87..2eed9a12 100644 --- a/sources/test/jest/cache-cleanup.test.ts +++ b/sources/test/jest/cache-cleanup.test.ts @@ -1,5 +1,6 @@ import * as exec from '@actions/exec' import * as core from '@actions/core' +import * as glob from '@actions/glob' import fs from 'fs' import path from 'path' import {CacheCleaner} from '../../src/caching/cache-cleaner' @@ -14,7 +15,7 @@ test('will cleanup unused dependency jars and build-cache entries', async () => await runGradleBuild(projectRoot, 'build', '3.1') - await cacheCleaner.prepare() + const timestamp = await cacheCleaner.prepare() await runGradleBuild(projectRoot, 'build', '3.1.1') @@ -26,7 +27,7 @@ test('will cleanup unused dependency jars and build-cache entries', async () => expect(fs.existsSync(commonsMath311)).toBe(true) expect(fs.readdirSync(buildCacheDir).length).toBe(4) // gc.properties, build-cache-1.lock, and 2 task entries - await cacheCleaner.forceCleanup() + await cacheCleaner.forceCleanupFilesOlderThan(timestamp) expect(fs.existsSync(commonsMath31)).toBe(false) expect(fs.existsSync(commonsMath311)).toBe(true) @@ -42,25 +43,39 @@ test('will cleanup unused gradle versions', async () => { // Initialize HOME with 2 different Gradle versions await runGradleWrapperBuild(projectRoot, 'build') await runGradleBuild(projectRoot, 'build') - - await cacheCleaner.prepare() + + const timestamp = await cacheCleaner.prepare() // Run with only one of these versions await runGradleBuild(projectRoot, 'build') const gradle802 = path.resolve(gradleUserHome, "caches/8.0.2") + const transforms3 = path.resolve(gradleUserHome, "caches/transforms-3") + const metadata100 = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.100") const wrapper802 = path.resolve(gradleUserHome, "wrapper/dists/gradle-8.0.2-bin") const gradleCurrent = path.resolve(gradleUserHome, "caches/8.8") + const metadataCurrent = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.106") expect(fs.existsSync(gradle802)).toBe(true) + expect(fs.existsSync(transforms3)).toBe(true) + expect(fs.existsSync(metadata100)).toBe(true) expect(fs.existsSync(wrapper802)).toBe(true) + expect(fs.existsSync(gradleCurrent)).toBe(true) + expect(fs.existsSync(metadataCurrent)).toBe(true) - await cacheCleaner.forceCleanup() + // The wrapper won't be removed if it was recently downloaded. Age it. + setUtimes(wrapper802, new Date(Date.now() - 48 * 60 * 60 * 1000)) + + await cacheCleaner.forceCleanupFilesOlderThan(timestamp) expect(fs.existsSync(gradle802)).toBe(false) + expect(fs.existsSync(transforms3)).toBe(false) + expect(fs.existsSync(metadata100)).toBe(false) expect(fs.existsSync(wrapper802)).toBe(false) + expect(fs.existsSync(gradleCurrent)).toBe(true) + expect(fs.existsSync(metadataCurrent)).toBe(true) }) async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise { @@ -86,3 +101,9 @@ function prepareTestProject(): string { return projectRoot } +async function setUtimes(pattern: string, timestamp: Date): Promise { + const globber = await glob.create(pattern) + for await (const file of globber.globGenerator()) { + fs.utimesSync(file, timestamp, timestamp) + } +}