gradle-build-action/src/cache-gradle-user-home.ts
Daz DeBoer 45ef022607
Always initialize Gradle User Home with init script
The generated gradle.properties file and init script are not cached,
so need to be generated even when restoring from cache.
2021-11-27 20:45:49 -07:00

308 lines
11 KiB
TypeScript

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> {
await this.reportGradleUserHomeSize('as restored from cache')
await this.restoreArtifactBundles(listener)
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> {
await this.reportGradleUserHomeSize('before saving common artifacts')
this.removeExcludedPaths()
await this.saveArtifactBundles(listener)
await this.reportGradleUserHomeSize(
"after saving common artifacts (only 'caches' and 'notifications' will be stored)"
)
}
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}`)
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)]))
}
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
})
core.info(`Gradle User Home (directories >5M): ${label}`)
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 {
def scanFile = new File("gradle-build-scan.txt")
buildScanPublished { buildScan ->
scanFile.text = buildScan.buildScanUri
}
}
}
`
)
}