Use Gradle 8.8 features for cleanup

Gradle 8.8 introduces new features that allow us to avoid using
timestamp manipulation to force the cleanup of the Gradle User Home directory.

This solution is simpler and more robust, but relies on Gradle 8.8+ always being
used for the cache cleanup operation.

Fixes #24
This commit is contained in:
daz 2024-03-21 19:03:05 -06:00
parent 169bec5d8b
commit 95ef72241e
No known key found for this signature in database
2 changed files with 58 additions and 42 deletions

View File

@ -1,5 +1,4 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import {provisionAndMaybeExecute} from '../execution/gradle' import {provisionAndMaybeExecute} from '../execution/gradle'
@ -13,25 +12,20 @@ export class CacheCleaner {
this.tmpDir = tmpDir this.tmpDir = tmpDir
} }
async prepare(): Promise<void> { async prepare(): Promise<string> {
// Reset the file-access journal so that files appear not to have been used recently // Save the current timestamp
fs.rmSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true, force: true}) const timestamp = Date.now().toString()
fs.mkdirSync(path.resolve(this.gradleUserHome, 'caches/journal-1'), {recursive: true}) core.saveState('clean-timestamp', timestamp)
fs.writeFileSync( return timestamp
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 forceCleanup(): Promise<void> { async forceCleanup(): Promise<void> {
// Age all 'gc' files so that cache cleanup will run immediately. const cleanTimestamp = core.getState('clean-timestamp')
await this.ageAllFiles('gc.properties') await this.forceCleanupFilesOlderThan(cleanTimestamp)
}
async forceCleanupFilesOlderThan(cleanTimestamp: string): Promise<void> {
core.info(`Cleaning up caches before ${cleanTimestamp}`)
// Run a dummy Gradle build to trigger cache cleanup // Run a dummy Gradle build to trigger cache cleanup
const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project') const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project')
@ -40,11 +34,31 @@ export class CacheCleaner {
path.resolve(cleanupProjectDir, 'settings.gradle'), path.resolve(cleanupProjectDir, 'settings.gradle'),
'rootProject.name = "dummy-cleanup-project"' '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") {}') fs.writeFileSync(path.resolve(cleanupProjectDir, 'build.gradle'), 'task("noop") {}')
await provisionAndMaybeExecute('current', cleanupProjectDir, [ await provisionAndMaybeExecute('current', cleanupProjectDir, [
'-g', '-g',
this.gradleUserHome, this.gradleUserHome,
'-I',
'init.gradle',
'--quiet', '--quiet',
'--no-daemon', '--no-daemon',
'--no-scan', '--no-scan',
@ -53,23 +67,4 @@ export class CacheCleaner {
'noop' 'noop'
]) ])
} }
private async ageAllFiles(fileName = '*'): Promise<void> {
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<void> {
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<void> {
const globber = await glob.create(pattern, {
implicitDescendants: false
})
for await (const file of globber.globGenerator()) {
fs.utimesSync(file, timestamp, timestamp)
}
}
} }

View File

@ -1,5 +1,6 @@
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as glob from '@actions/glob'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import {CacheCleaner} from '../../src/caching/cache-cleaner' 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 runGradleBuild(projectRoot, 'build', '3.1')
await cacheCleaner.prepare() const timestamp = await cacheCleaner.prepare()
await runGradleBuild(projectRoot, 'build', '3.1.1') 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.existsSync(commonsMath311)).toBe(true)
expect(fs.readdirSync(buildCacheDir).length).toBe(4) // gc.properties, build-cache-1.lock, and 2 task entries 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(commonsMath31)).toBe(false)
expect(fs.existsSync(commonsMath311)).toBe(true) expect(fs.existsSync(commonsMath311)).toBe(true)
@ -42,25 +43,39 @@ test('will cleanup unused gradle versions', async () => {
// Initialize HOME with 2 different Gradle versions // Initialize HOME with 2 different Gradle versions
await runGradleWrapperBuild(projectRoot, 'build') await runGradleWrapperBuild(projectRoot, 'build')
await runGradleBuild(projectRoot, 'build') await runGradleBuild(projectRoot, 'build')
await cacheCleaner.prepare() const timestamp = await cacheCleaner.prepare()
// Run with only one of these versions // Run with only one of these versions
await runGradleBuild(projectRoot, 'build') await runGradleBuild(projectRoot, 'build')
const gradle802 = path.resolve(gradleUserHome, "caches/8.0.2") 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 wrapper802 = path.resolve(gradleUserHome, "wrapper/dists/gradle-8.0.2-bin")
const gradleCurrent = path.resolve(gradleUserHome, "caches/8.8") 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(gradle802)).toBe(true)
expect(fs.existsSync(transforms3)).toBe(true)
expect(fs.existsSync(metadata100)).toBe(true)
expect(fs.existsSync(wrapper802)).toBe(true) expect(fs.existsSync(wrapper802)).toBe(true)
expect(fs.existsSync(gradleCurrent)).toBe(true)
await cacheCleaner.forceCleanup() expect(fs.existsSync(gradleCurrent)).toBe(true)
expect(fs.existsSync(metadataCurrent)).toBe(true)
// 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(gradle802)).toBe(false)
expect(fs.existsSync(transforms3)).toBe(false)
expect(fs.existsSync(metadata100)).toBe(false)
expect(fs.existsSync(wrapper802)).toBe(false) expect(fs.existsSync(wrapper802)).toBe(false)
expect(fs.existsSync(gradleCurrent)).toBe(true) expect(fs.existsSync(gradleCurrent)).toBe(true)
expect(fs.existsSync(metadataCurrent)).toBe(true)
}) })
async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise<void> { async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise<void> {
@ -86,3 +101,9 @@ function prepareTestProject(): string {
return projectRoot return projectRoot
} }
async function setUtimes(pattern: string, timestamp: Date): Promise<void> {
const globber = await glob.create(pattern)
for await (const file of globber.globGenerator()) {
fs.utimesSync(file, timestamp, timestamp)
}
}