Skip to content

Commit

Permalink
Use Gradle 8.8 features for Gradle Home cleanup (#272)
Browse files Browse the repository at this point in the history
Fixes #33
Fixes #24
Fixes #46 
Fixes #169
  • Loading branch information
bigdaz committed Jun 28, 2024
2 parents b9abb7b + 621f3b3 commit dad038d
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 50 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/integ-test-cache-cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
- name: Build with 3.1
working-directory: sources/test/jest/resources/cache-cleanup
run: gradle --no-daemon --build-cache -Dcommons_math3_version="3.1" build
run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1" build

# Second build will use the cache from the first build, but cleanup should remove unused artifacts
assemble-build:
Expand All @@ -58,7 +58,7 @@ jobs:
gradle-home-cache-cleanup: true
- name: Build with 3.1.1
working-directory: sources/test/jest/resources/cache-cleanup
run: gradle --no-daemon --build-cache -Dcommons_math3_version="3.1.1" build
run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1.1" build

check-clean-cache:
needs: assemble-build
Expand All @@ -78,7 +78,9 @@ jobs:
with:
cache-read-only: true
- name: Report Gradle User Home
run: du -hc ~/.gradle/caches/modules-2
run: |
du -hc ~/.gradle/caches/modules-2
du -hc ~/.gradle/wrapper/dists
- name: Verify cleaned cache
shell: bash
run: |
Expand All @@ -90,3 +92,7 @@ jobs:
echo "::error ::Should NOT find commons-math3 3.1 in cache"
exit 1
fi
if [ ! -e ~/.gradle/wrapper/dists/gradle-8.0.2-bin ]; then
echo "::error ::Should find gradle-8.0.2 in wrapper/dists"
exit 1
fi
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ Example running a single job:
`./build act -W .github/workflows/integ-test-caching-config.yml -j cache-disabled-pre-existing-gradle-home`

Known issues:
- `integ-test-cache-cleanup.yml` fails because `gradle` is not installed on the runner. Should be fixed by #33.
- `integ-test-detect-java-toolchains.yml` fails when running on a `linux/amd64` container, since the expected pre-installed JDKs are not present. Should be fixed by #89.
- `act` is not yet compatible with `actions/upload-artifact@v4` (or related toolkit functions)
- See https://github.com/nektos/act/pull/2224
Expand Down
83 changes: 42 additions & 41 deletions sources/src/caching/cache-cleaner.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as glob from '@actions/glob'
import fs from 'fs'
import path from 'path'
import {provisionAndMaybeExecute} from '../execution/gradle'

export class CacheCleaner {
private readonly gradleUserHome: string
Expand All @@ -13,25 +12,20 @@ export class CacheCleaner {
this.tmpDir = tmpDir
}

async prepare(): Promise<void> {
// 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<string> {
// Save the current timestamp
const timestamp = Date.now().toString()
core.saveState('clean-timestamp', timestamp)
return timestamp
}

async forceCleanup(): Promise<void> {
// 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<void> {
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')
Expand All @@ -40,30 +34,37 @@ 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") {}')

const gradleCommand = `gradle -g ${this.gradleUserHome} --no-daemon --build-cache --no-scan --quiet -DGITHUB_DEPENDENCY_GRAPH_ENABLED=false noop`
await exec.exec(gradleCommand, [], {
cwd: cleanupProjectDir
})
}

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)
}
await provisionAndMaybeExecute('current', cleanupProjectDir, [
'-g',
this.gradleUserHome,
'-I',
'init.gradle',
'--info',
'--no-daemon',
'--no-scan',
'--build-cache',
'-DGITHUB_DEPENDENCY_GRAPH_ENABLED=false',
'noop'
])
}
}
31 changes: 26 additions & 5 deletions sources/test/jest/cache-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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')

Expand All @@ -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)
Expand All @@ -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<void> {
Expand All @@ -86,3 +101,9 @@ function prepareTestProject(): string {
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)
}
}

0 comments on commit dad038d

Please sign in to comment.