gradle-build-action/src/cache-gradle-user-home.ts

318 lines
11 KiB
TypeScript
Raw Normal View History

import path from 'path'
import fs from 'fs'
import os from 'os'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import * as exec from '@actions/exec'
import {AbstractCache, CacheEntryListener, CacheListener} from './cache-base'
import {getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils'
const META_FILE_DIR = '.gradle-build-action'
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
const EXCLUDE_PATHS_PARAMETER = 'gradle-home-cache-excludes'
const ARTIFACT_BUNDLES_PARAMETER = 'gradle-home-cache-artifact-bundles'
export class GradleUserHomeCache extends AbstractCache {
private gradleUserHome: string
constructor(rootDir: string) {
super('gradle', 'Gradle User Home')
this.gradleUserHome = this.determineGradleUserHome(rootDir)
}
init(): void {
this.debug(`Initializing Gradle User Home with properties and init script: ${this.gradleUserHome}`)
initializeGradleUserHome(this.gradleUserHome)
}
async afterRestore(listener: CacheListener): Promise<void> {
2021-10-15 11:11:05 -06:00
await this.reportGradleUserHomeSize('as restored from cache')
await this.restoreArtifactBundles(listener)
2021-10-15 11:11:05 -06:00
await this.reportGradleUserHomeSize('after restoring common artifacts')
}
private async restoreArtifactBundles(listener: CacheListener): Promise<void> {
const processes: Promise<void>[] = []
const bundleMetaFiles = await this.getBundleMetaFiles()
const bundlePatterns = this.getArtifactBundles()
// Iterate over all bundle meta files and try to restore
for (const bundleMetaFile of bundleMetaFiles) {
const bundle = path.basename(bundleMetaFile, '.cache')
const entryListener = listener.entry(bundle)
const bundlePattern = bundlePatterns.get(bundle)
// Handle case where the 'artifactBundlePatterns' have been changed
if (bundlePattern === undefined) {
core.info(`Found bundle metafile for ${bundle} but no such bundle defined`)
entryListener.markRequested('BUNDLE_NOT_CONFIGURED')
tryDelete(bundleMetaFile)
} else {
const p = this.restoreArtifactBundle(bundle, bundlePattern, bundleMetaFile, entryListener)
// Run sequentially when debugging enabled
if (this.cacheDebuggingEnabled) {
await p
}
processes.push(p)
}
}
await Promise.all(processes)
}
private async restoreArtifactBundle(
bundle: string,
bundlePattern: string,
bundleMetaFile: string,
listener: CacheEntryListener
): Promise<void> {
const cacheKey = fs.readFileSync(bundleMetaFile, 'utf-8').trim()
listener.markRequested(cacheKey)
const restoredEntry = await this.restoreCache([bundlePattern], cacheKey)
if (restoredEntry) {
core.info(`Restored ${bundle} with key ${cacheKey} to ${bundlePattern}`)
listener.markRestored(restoredEntry.key, restoredEntry.size)
} else {
core.info(`Did not restore ${bundle} with key ${cacheKey} to ${bundlePattern}`)
tryDelete(bundleMetaFile)
}
}
private getBundleMetaFile(name: string): string {
return path.resolve(this.gradleUserHome, META_FILE_DIR, `${name}.cache`)
}
private async getBundleMetaFiles(): Promise<string[]> {
const metaFiles = path.resolve(this.gradleUserHome, META_FILE_DIR, '*.cache')
const globber = await glob.create(metaFiles)
const bundleFiles = await globber.glob()
return bundleFiles
}
async beforeSave(listener: CacheListener): Promise<void> {
2021-10-15 11:11:05 -06:00
await this.reportGradleUserHomeSize('before saving common artifacts')
this.removeExcludedPaths()
await this.saveArtifactBundles(listener)
2021-10-16 10:15:40 -06:00
await this.reportGradleUserHomeSize(
"after saving common artifacts (only 'caches' and 'notifications' will be stored)"
2021-10-16 10:15:40 -06:00
)
}
private removeExcludedPaths(): void {
const rawPaths: string[] = core.getMultilineInput(EXCLUDE_PATHS_PARAMETER)
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
for (const p of resolvedPaths) {
this.debug(`Deleting excluded path: ${p}`)
tryDelete(p)
}
}
private async saveArtifactBundles(listener: CacheListener): Promise<void> {
const processes: Promise<void>[] = []
for (const [bundle, pattern] of this.getArtifactBundles()) {
const entryListener = listener.entry(bundle)
const p = this.saveArtifactBundle(bundle, pattern, entryListener)
// Run sequentially when debugging enabled
if (this.cacheDebuggingEnabled) {
await p
}
processes.push(p)
}
await Promise.all(processes)
}
private async saveArtifactBundle(
bundle: string,
artifactPath: string,
listener: CacheEntryListener
): Promise<void> {
const bundleMetaFile = this.getBundleMetaFile(bundle)
const globber = await glob.create(artifactPath, {
implicitDescendants: false,
followSymbolicLinks: false
})
const bundleFiles = await globber.glob()
// Handle no matching files
if (bundleFiles.length === 0) {
this.debug(`No files found to cache for ${bundle}`)
if (fs.existsSync(bundleMetaFile)) {
tryDelete(bundleMetaFile)
}
return
}
const previouslyRestoredKey = fs.existsSync(bundleMetaFile)
? fs.readFileSync(bundleMetaFile, 'utf-8').trim()
: ''
const cacheKey = this.createCacheKey(bundle, bundleFiles)
if (previouslyRestoredKey === cacheKey) {
this.debug(`No change to previously restored ${bundle}. Not caching.`)
} else {
core.info(`Caching ${bundle} with cache key: ${cacheKey}`)
const savedEntry = await this.saveCache([artifactPath], cacheKey)
if (savedEntry !== undefined) {
this.writeBundleMetaFile(bundleMetaFile, cacheKey)
listener.markSaved(savedEntry.key, savedEntry.size)
}
}
for (const file of bundleFiles) {
tryDelete(file)
}
}
protected createCacheKey(bundle: string, files: string[]): string {
const cacheKeyPrefix = getCacheKeyPrefix()
const relativeFiles = files.map(x => path.relative(this.gradleUserHome, x))
const key = hashFileNames(relativeFiles)
this.debug(`Generating cache key for ${bundle} from files: ${relativeFiles}`)
2021-10-15 11:11:05 -06:00
return `${cacheKeyPrefix}${bundle}-${key}`
}
private writeBundleMetaFile(metaFile: string, cacheKey: string): void {
this.debug(`Writing bundle metafile: ${metaFile}`)
const dirName = path.dirname(metaFile)
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName)
}
fs.writeFileSync(metaFile, cacheKey)
}
protected determineGradleUserHome(rootDir: string): string {
const customGradleUserHome = process.env['GRADLE_USER_HOME']
if (customGradleUserHome) {
return path.resolve(rootDir, customGradleUserHome)
}
return path.resolve(os.homedir(), '.gradle')
}
protected cacheOutputExists(): boolean {
// Need to check for 'caches' directory to avoid incorrect detection on MacOS agents
const dir = path.resolve(this.gradleUserHome, 'caches')
return fs.existsSync(dir)
}
protected getCachePath(): string[] {
const rawPaths: string[] = core.getMultilineInput(INCLUDE_PATHS_PARAMETER)
rawPaths.push(META_FILE_DIR)
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
this.debug(`Using cache paths: ${resolvedPaths}`)
return resolvedPaths
}
private resolveCachePath(rawPath: string): string {
if (rawPath.startsWith('!')) {
const resolved = this.resolveCachePath(rawPath.substring(1))
return `!${resolved}`
}
return path.resolve(this.gradleUserHome, rawPath)
}
private getArtifactBundles(): Map<string, string> {
const artifactBundleDefinition = core.getInput(ARTIFACT_BUNDLES_PARAMETER)
this.debug(`Using artifact bundle definition: ${artifactBundleDefinition}`)
const artifactBundles = JSON.parse(artifactBundleDefinition)
return new Map(Array.from(artifactBundles, ([key, value]) => [key, path.resolve(this.gradleUserHome, value)]))
}
2021-10-15 11:11:05 -06:00
private async reportGradleUserHomeSize(label: string): Promise<void> {
if (!this.cacheDebuggingEnabled) {
return
}
if (!fs.existsSync(this.gradleUserHome)) {
return
}
const result = await exec.getExecOutput('du', ['-h', '-c', '-t', '5M'], {
cwd: this.gradleUserHome,
silent: true,
ignoreReturnCode: true
})
2021-10-15 11:11:05 -06:00
2021-10-16 10:15:40 -06:00
core.info(`Gradle User Home (directories >5M): ${label}`)
2021-10-15 11:11:05 -06:00
core.info(
result.stdout
.trimEnd()
.replace(/\t/g, ' ')
.split('\n')
.map(it => {
return ` ${it}`
})
.join('\n')
)
core.info('-----------------------')
}
}
function initializeGradleUserHome(gradleUserHome: string): void {
fs.mkdirSync(gradleUserHome, {recursive: true})
const propertiesFile = path.resolve(gradleUserHome, 'gradle.properties')
fs.writeFileSync(propertiesFile, 'org.gradle.daemon=false')
const initScript = path.resolve(gradleUserHome, 'init.gradle')
fs.writeFileSync(
initScript,
`
import org.gradle.util.GradleVersion
// Don't run against the included builds (if the main build has any).
def isTopLevelBuild = gradle.getParent() == null
if (isTopLevelBuild) {
def version = GradleVersion.current().baseVersion
def atLeastGradle4 = version >= GradleVersion.version("4.0")
def atLeastGradle6 = version >= GradleVersion.version("6.0")
if (atLeastGradle6) {
settingsEvaluated { settings ->
if (settings.pluginManager.hasPlugin("com.gradle.enterprise")) {
registerCallbacks(settings.extensions["gradleEnterprise"].buildScan, settings.rootProject.name)
}
}
} else if (atLeastGradle4) {
projectsEvaluated { gradle ->
if (gradle.rootProject.pluginManager.hasPlugin("com.gradle.build-scan")) {
registerCallbacks(gradle.rootProject.extensions["buildScan"], gradle.rootProject.name)
}
}
}
}
def registerCallbacks(buildScanExtension, rootProjectName) {
buildScanExtension.with {
2021-11-28 08:03:42 -07:00
def buildOutcome = ""
def scanFile = new File("gradle-build-scan.txt")
2021-11-28 08:03:42 -07:00
buildFinished { result ->
buildOutcome = result.failure == null ? " succeeded" : " failed"
}
buildScanPublished { buildScan ->
scanFile.text = buildScan.buildScanUri
2021-11-28 08:03:42 -07:00
// Send commands directly to GitHub Actions via STDOUT.
println("::notice title=Build '\${rootProjectName}'\${buildOutcome}::\${buildScan.buildScanUri}")
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
}
}
}
`
)
}