Merge pull request #131 from gradle/cache-jdks

- Cache downloaded Java Toolchains
- Treat configuration-cache entries as extracted entries
This commit is contained in:
Daz DeBoer 2021-12-30 16:19:02 -07:00 committed by GitHub
commit f804351b51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1037 additions and 588 deletions

View file

@ -22,21 +22,13 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: ./ uses: ./
with: with:
# Add "wrapper" to main cache entry and remove 'wrapper-zips' cache entry # Add "enterprise" to main cache entry but omit "notifications"
# Exclude build-cache from main cache entry
gradle-home-cache-includes: | gradle-home-cache-includes: |
caches caches
notifications enterprise
wrapper # Exclude build-cache from main cache entry
gradle-home-cache-excludes: | gradle-home-cache-excludes: |
caches/build-cache-1 caches/build-cache-1
gradle-home-extracted-cache-entries: |
[
["generated-gradle-jars", "caches/*/generated-gradle-jars/*.jar"],
["dependencies", "caches/modules-*/files-*/*/*/*/*/"],
["instrumented-jars", "caches/jars-*/*/"],
["kotlin-dsl", "caches/*/kotlin-dsl/*/*/"]
]
- name: Build using Gradle wrapper - name: Build using Gradle wrapper
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test run: ./gradlew test
@ -57,17 +49,9 @@ jobs:
# Use the same configuration when restoring state from cache # Use the same configuration when restoring state from cache
gradle-home-cache-includes: | gradle-home-cache-includes: |
caches caches
notifications enterprise
wrapper
gradle-home-cache-excludes: | gradle-home-cache-excludes: |
caches/build-cache-1 caches/build-cache-1
gradle-home-extracted-cache-entries: |
[
["generated-gradle-jars", "caches/*/generated-gradle-jars/*.jar"],
["dependencies", "caches/modules-*/files-*/*/*/*/*/"],
["instrumented-jars", "caches/jars-*/*/"],
["kotlin-dsl", "caches/*/kotlin-dsl/*/*/"]
]
cache-read-only: true cache-read-only: true
- name: Execute Gradle build with --offline - name: Execute Gradle build with --offline
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl

View file

@ -7,13 +7,14 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
jobs: jobs:
# Run initial Gradle builds to push initial cache entries # Run initial Gradle builds to push initial cache entries
# These builds should start fresh without cache hits, due to the seed injected into the cache key above. # These builds should start fresh without cache hits, due to the seed injected into the cache key above.
seed-build: seed-build-groovy:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy-
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
@ -26,12 +27,11 @@ jobs:
- name: Groovy build with configuration-cache enabled - name: Groovy build with configuration-cache enabled
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test --configuration-cache run: ./gradlew test --configuration-cache
- name: Kotlin build with configuration-cache enabled
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache
configuration-cache-groovy: configuration-cache-groovy:
needs: seed-build env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy-
needs: seed-build-groovy
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
@ -49,29 +49,11 @@ jobs:
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test --configuration-cache run: ./gradlew test --configuration-cache
# Test restore configuration-cache from the second build invocation
configuration-cache-kotlin:
needs: seed-build
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
with:
cache-read-only: true
- name: Execute Gradle build and verify cached configuration
env:
VERIFY_CACHED_CONFIGURATION: true
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache
# Check that the build can run when no extracted cache entries are restored # Check that the build can run when no extracted cache entries are restored
no-extracted-cache-entries-restored: no-extracted-cache-entries-restored:
needs: seed-build env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-groovy-
needs: seed-build-groovy
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
@ -79,11 +61,67 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Gradle with no cache extracted cache entries restored - name: Setup Gradle with no extracted cache entries restored
uses: ./ uses: ./
env:
GRADLE_BUILD_ACTION_SKIP_RESTORE: "generated-gradle-jars|wrapper-zips|java-toolchains|instrumented-jars|dependencies|kotlin-dsl"
with: with:
cache-read-only: true cache-read-only: true
gradle-home-extracted-cache-entries: '[]'
- name: Check execute Gradle build with configuration cache enabled (but not restored) - name: Check execute Gradle build with configuration cache enabled (but not restored)
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test --configuration-cache run: ./gradlew test --configuration-cache
seed-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-kotlin-
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
- name: Kotlin build with configuration-cache enabled
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew help --configuration-cache
modify-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-kotlin-
needs: seed-build-kotlin
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
- name: Kotlin build with configuration-cache enabled
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache
# Test restore configuration-cache from the third build invocation
configuration-cache-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-kotlin-
needs: modify-build-kotlin
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
with:
cache-read-only: true
- name: Execute Gradle build and verify cached configuration
env:
VERIFY_CACHED_CONFIGURATION: true
working-directory: __tests__/samples/kotlin-dsl
run: ./gradlew test --configuration-cache

View file

@ -72,9 +72,10 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup Gradle with no extracted cache entries restored - name: Setup Gradle with no extracted cache entries restored
uses: ./ uses: ./
env:
GRADLE_BUILD_ACTION_SKIP_RESTORE: "generated-gradle-jars|wrapper-zips|java-toolchains|instrumented-jars|dependencies|kotlin-dsl"
with: with:
cache-read-only: true cache-read-only: true
gradle-home-extracted-cache-entries: '[]'
- name: Check executee Gradle build - name: Check executee Gradle build
working-directory: __tests__/samples/groovy-dsl working-directory: __tests__/samples/groovy-dsl
run: ./gradlew test run: ./gradlew test

View file

@ -0,0 +1,43 @@
name: Test save/restore java toolchains
on:
pull_request:
types: [assigned, review_requested]
push:
workflow_dispatch:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-
jobs:
seed-build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
- name: Build using Gradle wrapper
working-directory: __tests__/samples/java-toolchain
run: ./gradlew test --info
# Test that the gradle-user-home cache will cache the toolchain, by running build with --offline
toolchain-cache:
needs: seed-build
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Setup Gradle
uses: ./
with:
cache-read-only: true
- name: Execute Gradle build with --offline
working-directory: __tests__/samples/java-toolchain
run: ./gradlew test --info --offline

View file

@ -0,0 +1,17 @@
plugins {
id 'java'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(16)
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation('junit:junit:4.12')
}

View file

@ -0,0 +1 @@
org.gradle.caching=true

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
__tests__/samples/java-toolchain/gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View file

@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1 @@
rootProject.name = 'basic'

View file

@ -0,0 +1,10 @@
package basic;
import org.junit.Test;
public class BasicTest {
@Test
public void test() {
assert true;
}
}

View file

@ -55,17 +55,6 @@ inputs:
description: Used to uniquely identify the current job invocation. Defaults to the matrix values for this job; this should not be overridden by users (INTERNAL). description: Used to uniquely identify the current job invocation. Defaults to the matrix values for this job; this should not be overridden by users (INTERNAL).
required: false required: false
default: ${{ toJSON(matrix) }} default: ${{ toJSON(matrix) }}
gradle-home-extracted-cache-entries:
description: Names and patterns of artifacts in Gradle User Home to cache separately. (EXPERIMENTAL - may be changed/removed without notice)
required: false
default: |
[
["generated-gradle-jars", "caches/*/generated-gradle-jars/*.jar"],
["wrapper-zips", "wrapper/dists/*/*/*.zip"],
["dependencies", "caches/modules-*/files-*/*/*/*/*/"],
["instrumented-jars", "caches/jars-*/*/"],
["kotlin-dsl", "caches/*/kotlin-dsl/*/*/"]
]
outputs: outputs:
build-scan-url: build-scan-url:
@ -75,7 +64,6 @@ runs:
using: 'node12' using: 'node12'
main: 'dist/main/index.js' main: 'dist/main/index.js'
post: 'dist/post/index.js' post: 'dist/post/index.js'
post-if: success()
branding: branding:
icon: 'box' icon: 'box'

2
dist/main/index.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/post/index.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,26 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as cache from '@actions/cache' import * as exec from '@actions/exec'
import * as github from '@actions/github' import * as github from '@actions/github'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import {CacheListener} from './cache-reporting' import {CacheListener} from './cache-reporting'
import {isCacheDebuggingEnabled, getCacheKeyPrefix, determineJobContext, handleCacheFailure} from './cache-utils' import {
getCacheKeyPrefix,
determineJobContext,
saveCache,
restoreCache,
cacheDebug,
isCacheDebuggingEnabled,
tryDelete
} from './cache-utils'
import {ConfigurationCacheEntryExtractor, GradleHomeEntryExtractor} from './cache-extract-entries'
const CACHE_PROTOCOL_VERSION = 'v5-' const CACHE_PROTOCOL_VERSION = 'v5-'
export const META_FILE_DIR = '.gradle-build-action' export const META_FILE_DIR = '.gradle-build-action'
export const PROJECT_ROOTS_FILE = 'project-roots.txt' export const PROJECT_ROOTS_FILE = 'project-roots.txt'
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
const EXCLUDE_PATHS_PARAMETER = 'gradle-home-cache-excludes'
/** /**
* Represents a key used to restore a cache entry. * Represents a key used to restore a cache entry.
@ -62,22 +73,20 @@ function generateCacheKey(cacheName: string): CacheKey {
return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs]) return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs])
} }
export abstract class AbstractCache { export class GradleStateCache {
private cacheName: string private cacheName: string
private cacheDescription: string private cacheDescription: string
private cacheKeyStateKey: string private cacheKeyStateKey: string
private cacheResultStateKey: string private cacheResultStateKey: string
protected readonly gradleUserHome: string protected readonly gradleUserHome: string
protected readonly cacheDebuggingEnabled: boolean
constructor(gradleUserHome: string, cacheName: string, cacheDescription: string) { constructor(gradleUserHome: string) {
this.gradleUserHome = gradleUserHome this.gradleUserHome = gradleUserHome
this.cacheName = cacheName this.cacheName = 'gradle'
this.cacheDescription = cacheDescription this.cacheDescription = 'Gradle User Home'
this.cacheKeyStateKey = `CACHE_KEY_${cacheName}` this.cacheKeyStateKey = `CACHE_KEY_gradle`
this.cacheResultStateKey = `CACHE_RESULT_${cacheName}` this.cacheResultStateKey = `CACHE_RESULT_gradle`
this.cacheDebuggingEnabled = isCacheDebuggingEnabled()
} }
init(): void { init(): void {
@ -96,15 +105,16 @@ export abstract class AbstractCache {
async restore(listener: CacheListener): Promise<void> { async restore(listener: CacheListener): Promise<void> {
const entryListener = listener.entry(this.cacheDescription) const entryListener = listener.entry(this.cacheDescription)
const cacheKey = this.prepareCacheKey() const cacheKey = generateCacheKey(this.cacheName)
core.saveState(this.cacheKeyStateKey, cacheKey.key)
this.debug( cacheDebug(
`Requesting ${this.cacheDescription} with `Requesting ${this.cacheDescription} with
key:${cacheKey.key} key:${cacheKey.key}
restoreKeys:[${cacheKey.restoreKeys}]` restoreKeys:[${cacheKey.restoreKeys}]`
) )
const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys) const cacheResult = await restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys)
entryListener.markRequested(cacheKey.key, cacheKey.restoreKeys) entryListener.markRequested(cacheKey.key, cacheKey.restoreKeys)
if (!cacheResult) { if (!cacheResult) {
@ -124,27 +134,16 @@ export abstract class AbstractCache {
} }
} }
prepareCacheKey(): CacheKey { /**
const cacheKey = generateCacheKey(this.cacheName) * Restore any extracted cache entries after the main Gradle User Home entry is restored.
core.saveState(this.cacheKeyStateKey, cacheKey.key) */
return cacheKey async afterRestore(listener: CacheListener): Promise<void> {
await this.debugReportGradleUserHomeSize('as restored from cache')
await new GradleHomeEntryExtractor(this.gradleUserHome).restore(listener)
await new ConfigurationCacheEntryExtractor(this.gradleUserHome).restore(listener)
await this.debugReportGradleUserHomeSize('after restoring common artifacts')
} }
protected async restoreCache(
cachePath: string[],
cacheKey: string,
cacheRestoreKeys: string[] = []
): Promise<cache.CacheEntry | undefined> {
try {
return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys)
} catch (error) {
handleCacheFailure(error, `Failed to restore ${cacheKey}`)
return undefined
}
}
protected async afterRestore(_listener: CacheListener): Promise<void> {}
/** /**
* Saves the cache entry based on the current cache key unless the cache was restored with the exact key, * Saves the cache entry based on the current cache key unless the cache was restored with the exact key,
* in which case we cannot overwrite it. * in which case we cannot overwrite it.
@ -171,7 +170,7 @@ export abstract class AbstractCache {
core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKeyFromRestore}`) core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKeyFromRestore}`)
const cachePath = this.getCachePath() const cachePath = this.getCachePath()
const savedEntry = await this.saveCache(cachePath, cacheKeyFromRestore) const savedEntry = await saveCache(cachePath, cacheKeyFromRestore)
if (savedEntry) { if (savedEntry) {
listener.entry(this.cacheDescription).markSaved(savedEntry.key, savedEntry.size) listener.entry(this.cacheDescription).markSaved(savedEntry.key, savedEntry.size)
@ -180,25 +179,155 @@ export abstract class AbstractCache {
return return
} }
protected async beforeSave(_listener: CacheListener): Promise<void> {} /**
* Extract and save any defined extracted cache entries prior to the main Gradle User Home entry being saved.
protected async saveCache(cachePath: string[], cacheKey: string): Promise<cache.CacheEntry | undefined> { */
try { async beforeSave(listener: CacheListener): Promise<void> {
return await cache.saveCache(cachePath, cacheKey) await this.debugReportGradleUserHomeSize('before saving common artifacts')
} catch (error) { this.deleteExcludedPaths()
handleCacheFailure(error, `Failed to save cache entry ${cacheKey}`) await Promise.all([
} new GradleHomeEntryExtractor(this.gradleUserHome).extract(listener),
return undefined new ConfigurationCacheEntryExtractor(this.gradleUserHome).extract(listener)
])
await this.debugReportGradleUserHomeSize(
"after extracting common artifacts (only 'caches' and 'notifications' will be stored)"
)
} }
protected debug(message: string): void { /**
if (this.cacheDebuggingEnabled) { * Delete any file paths that are excluded by the `gradle-home-cache-excludes` parameter.
core.info(message) */
} else { private deleteExcludedPaths(): void {
core.debug(message) const rawPaths: string[] = core.getMultilineInput(EXCLUDE_PATHS_PARAMETER)
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
for (const p of resolvedPaths) {
cacheDebug(`Deleting excluded path: ${p}`)
tryDelete(p)
} }
} }
protected abstract getCachePath(): string[] /**
protected abstract initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void * Determines the paths within Gradle User Home to cache.
* By default, this is the 'caches' and 'notifications' directories,
* but this can be overridden by the `gradle-home-cache-includes` parameter.
*/
protected getCachePath(): string[] {
const rawPaths: string[] = core.getMultilineInput(INCLUDE_PATHS_PARAMETER)
rawPaths.push(META_FILE_DIR)
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
cacheDebug(`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 initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
const propertiesFile = path.resolve(gradleUserHome, 'gradle.properties')
fs.writeFileSync(propertiesFile, 'org.gradle.daemon=false')
const buildScanCapture = path.resolve(initScriptsDir, 'build-scan-capture.init.gradle')
fs.writeFileSync(
buildScanCapture,
`import org.gradle.util.GradleVersion
// Only run again root build. Do not run against included builds.
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 buildOutcome = ""
def scanFile = new File("gradle-build-scan.txt")
buildFinished { result ->
buildOutcome = result.failure == null ? " succeeded" : " failed"
}
buildScanPublished { buildScan ->
scanFile.text = buildScan.buildScanUri
// Send commands directly to GitHub Actions via STDOUT.
def message = "Build '\${rootProjectName}'\${buildOutcome} - \${buildScan.buildScanUri}"
println("::notice ::\${message}")
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
}
}
}`
)
const projectRootCapture = path.resolve(initScriptsDir, 'project-root-capture.init.gradle')
fs.writeFileSync(
projectRootCapture,
`
// Only run again root build. Do not run against included builds.
def isTopLevelBuild = gradle.getParent() == null
if (isTopLevelBuild) {
settingsEvaluated { settings ->
def projectRootEntry = settings.rootDir.absolutePath + "\\n"
def projectRootList = new File(settings.gradle.gradleUserHomeDir, "${PROJECT_ROOTS_FILE}")
if (!projectRootList.exists() || !projectRootList.text.contains(projectRootEntry)) {
projectRootList << projectRootEntry
}
}
}`
)
}
/**
* When cache debugging is enabled, this method will give a detailed report
* of the Gradle User Home contents.
*/
private async debugReportGradleUserHomeSize(label: string): Promise<void> {
if (!isCacheDebuggingEnabled()) {
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('-----------------------')
}
} }

View file

@ -0,0 +1,376 @@
import path from 'path'
import fs from 'fs'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import {META_FILE_DIR, PROJECT_ROOTS_FILE} from './cache-base'
import {CacheEntryListener, CacheListener} from './cache-reporting'
import {
cacheDebug,
getCacheKeyPrefix,
hashFileNames,
isCacheDebuggingEnabled,
restoreCache,
saveCache,
tryDelete
} from './cache-utils'
const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE'
/**
* Represents the result of attempting to load or store an extracted cache entry.
* An undefined cacheKey indicates that the operation did not succeed.
* The collected results are then used to populate the `cache-metadata.json` file for later use.
*/
class ExtractedCacheEntry {
artifactType: string
pattern: string
cacheKey: string | undefined
constructor(artifactType: string, pattern: string, cacheKey: string | undefined) {
this.artifactType = artifactType
this.pattern = pattern
this.cacheKey = cacheKey
}
}
/**
* Representation of all of the extracted cache entries for this Gradle User Home.
* This object is persisted to JSON file in the Gradle User Home directory for storing,
* and subsequently used to restore the Gradle User Home.
*/
class ExtractedCacheEntryMetadata {
entries: ExtractedCacheEntry[] = []
}
/**
* The specification for a type of extracted cache entry.
*/
class ExtractedCacheEntryDefinition {
artifactType: string
pattern: string
bundle: boolean
uniqueFileNames = true
constructor(artifactType: string, pattern: string, bundle: boolean) {
this.artifactType = artifactType
this.pattern = pattern
this.bundle = bundle
}
withNonUniqueFileNames(): ExtractedCacheEntryDefinition {
this.uniqueFileNames = false
return this
}
}
/**
* Caches and restores the entire Gradle User Home directory, extracting entries containing common artifacts
* for more efficient storage.
*/
abstract class AbstractEntryExtractor {
protected readonly gradleUserHome: string
private extractorName: string
constructor(gradleUserHome: string, extractorName: string) {
this.gradleUserHome = gradleUserHome
this.extractorName = extractorName
}
/**
* Restores any artifacts that were cached separately, based on the information in the `cache-metadata.json` file.
* Each extracted cache entry is restored in parallel, except when debugging is enabled.
*/
async restore(listener: CacheListener): Promise<void> {
const previouslyExtractedCacheEntries = this.loadExtractedCacheEntries()
const processes: Promise<ExtractedCacheEntry>[] = []
for (const cacheEntry of previouslyExtractedCacheEntries) {
const artifactType = cacheEntry.artifactType
const entryListener = listener.entry(cacheEntry.pattern)
// Handle case where the extracted-cache-entry definitions have been changed
const skipRestore = process.env[SKIP_RESTORE_VAR] || ''
if (skipRestore.includes(artifactType)) {
core.info(`Not restoring extracted cache entry for ${artifactType}`)
entryListener.markRequested('SKIP_RESTORE')
} else {
processes.push(
this.awaitForDebugging(
this.restoreExtractedCacheEntry(
artifactType,
cacheEntry.cacheKey!,
cacheEntry.pattern,
entryListener
)
)
)
}
}
this.saveMetadataForCacheResults(await Promise.all(processes))
}
private async restoreExtractedCacheEntry(
artifactType: string,
cacheKey: string,
pattern: string,
listener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
listener.markRequested(cacheKey)
const restoredEntry = await restoreCache([pattern], cacheKey)
if (restoredEntry) {
core.info(`Restored ${artifactType} with key ${cacheKey} to ${pattern}`)
listener.markRestored(restoredEntry.key, restoredEntry.size)
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
} else {
core.info(`Did not restore ${artifactType} with key ${cacheKey} to ${pattern}`)
return new ExtractedCacheEntry(artifactType, pattern, undefined)
}
}
/**
* Saves any artifacts that are configured to be cached separately, based on the extracted cache entry definitions.
* Each entry is extracted and saved in parallel, except when debugging is enabled.
*/
async extract(listener: CacheListener): Promise<void> {
// Load the cache entry definitions (from config) and the previously restored entries (from persisted metadata file)
const cacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
cacheDebug(
`Extracting cache entries for ${this.extractorName}: ${JSON.stringify(cacheEntryDefinitions, null, 2)}`
)
const previouslyRestoredEntries = this.loadExtractedCacheEntries()
const cacheActions: Promise<ExtractedCacheEntry>[] = []
// For each cache entry definition, determine if it has already been restored, and if not, extract it
for (const cacheEntryDefinition of cacheEntryDefinitions) {
const artifactType = cacheEntryDefinition.artifactType
const pattern = cacheEntryDefinition.pattern
// Find all matching files for this cache entry definition
const globber = await glob.create(pattern, {
implicitDescendants: false,
followSymbolicLinks: false
})
const matchingFiles = await globber.glob()
if (matchingFiles.length === 0) {
cacheDebug(`No files found to cache for ${artifactType}`)
continue
}
if (cacheEntryDefinition.bundle) {
// For an extracted "bundle", use the defined pattern and cache all matching files in a single entry.
cacheActions.push(
this.awaitForDebugging(
this.saveExtractedCacheEntry(
matchingFiles,
artifactType,
pattern,
cacheEntryDefinition.uniqueFileNames,
previouslyRestoredEntries,
listener.entry(pattern)
)
)
)
} else {
// Otherwise cache each matching file in a separate entry, using the complete file path as the cache pattern.
for (const cacheFile of matchingFiles) {
cacheActions.push(
this.awaitForDebugging(
this.saveExtractedCacheEntry(
[cacheFile],
artifactType,
cacheFile,
cacheEntryDefinition.uniqueFileNames,
previouslyRestoredEntries,
listener.entry(cacheFile)
)
)
)
}
}
}
this.saveMetadataForCacheResults(await Promise.all(cacheActions))
}
private async saveExtractedCacheEntry(
matchingFiles: string[],
artifactType: string,
pattern: string,
uniqueFileNames: boolean,
previouslyRestoredEntries: ExtractedCacheEntry[],
entryListener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
const cacheKey = uniqueFileNames
? this.createCacheKeyFromFileNames(artifactType, matchingFiles)
: await this.createCacheKeyFromFileContents(artifactType, pattern)
const previouslyRestoredKey = previouslyRestoredEntries.find(
x => x.artifactType === artifactType && x.pattern === pattern
)?.cacheKey
if (previouslyRestoredKey === cacheKey) {
cacheDebug(`No change to previously restored ${artifactType}. Not saving.`)
} else {
core.info(`Caching ${artifactType} with path '${pattern}' and cache key: ${cacheKey}`)
const savedEntry = await saveCache([pattern], cacheKey)
if (savedEntry !== undefined) {
entryListener.markSaved(savedEntry.key, savedEntry.size)
}
}
for (const file of matchingFiles) {
tryDelete(file)
}
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
}
protected createCacheKeyFromFileNames(artifactType: string, files: string[]): string {
const cacheKeyPrefix = getCacheKeyPrefix()
const relativeFiles = files.map(x => path.relative(this.gradleUserHome, x))
const key = hashFileNames(relativeFiles)
cacheDebug(`Generating cache key for ${artifactType} from file names: ${relativeFiles}`)
return `${cacheKeyPrefix}${artifactType}-${key}`
}
protected async createCacheKeyFromFileContents(artifactType: string, pattern: string): Promise<string> {
const cacheKeyPrefix = getCacheKeyPrefix()
const key = await glob.hashFiles(pattern)
cacheDebug(`Generating cache key for ${artifactType} from files matching: ${pattern}`)
return `${cacheKeyPrefix}${artifactType}-${key}`
}
// Run actions sequentially if debugging is enabled
private async awaitForDebugging(p: Promise<ExtractedCacheEntry>): Promise<ExtractedCacheEntry> {
if (isCacheDebuggingEnabled()) {
await p
}
return p
}
/**
* Load information about the extracted cache entries previously restored/saved. This is loaded from the 'cache-metadata.json' file.
*/
protected loadExtractedCacheEntries(): ExtractedCacheEntry[] {
const cacheMetadataFile = this.getCacheMetadataFile()
if (!fs.existsSync(cacheMetadataFile)) {
return []
}
const filedata = fs.readFileSync(cacheMetadataFile, 'utf-8')
cacheDebug(`Loaded cache metadata: ${filedata}`)
const extractedCacheEntryMetadata = JSON.parse(filedata) as ExtractedCacheEntryMetadata
return extractedCacheEntryMetadata.entries
}
/**
* Saves information about the extracted cache entries into the 'cache-metadata.json' file.
*/
private saveMetadataForCacheResults(results: ExtractedCacheEntry[]): void {
const extractedCacheEntryMetadata = new ExtractedCacheEntryMetadata()
extractedCacheEntryMetadata.entries = results.filter(x => x.cacheKey !== undefined)
const filedata = JSON.stringify(extractedCacheEntryMetadata)
cacheDebug(`Saving cache metadata: ${filedata}`)
fs.writeFileSync(this.getCacheMetadataFile(), filedata, 'utf-8')
}
private getCacheMetadataFile(): string {
const actionMetadataDirectory = path.resolve(this.gradleUserHome, META_FILE_DIR)
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
return path.resolve(actionMetadataDirectory, `${this.extractorName}-entry-metadata.json`)
}
protected abstract getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[]
}
export class GradleHomeEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'gradle-home')
}
/**
* Return the extracted cache entry definitions, which determine which artifacts will be cached
* separately from the rest of the Gradle User Home cache entry.
*/
protected getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[] {
const entryDefinition = (
artifactType: string,
patterns: string[],
bundle: boolean
): ExtractedCacheEntryDefinition => {
const resolvedPattern = patterns.map(x => path.resolve(this.gradleUserHome, x)).join('\n')
return new ExtractedCacheEntryDefinition(artifactType, resolvedPattern, bundle)
}
return [
entryDefinition('generated-gradle-jars', ['caches/*/generated-gradle-jars/*.jar'], false),
entryDefinition('wrapper-zips', ['wrapper/dists/*/*/*.zip'], false),
entryDefinition('java-toolchains', ['jdks/*.zip', 'jdks/*.tar.gz'], false),
entryDefinition('dependencies', ['caches/modules-*/files-*/*/*/*/*'], true),
entryDefinition('instrumented-jars', ['caches/jars-*/*'], true),
entryDefinition('kotlin-dsl', ['caches/*/kotlin-dsl/*/*'], true)
]
}
}
export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'configuration-cache')
}
/**
* Handle the case where Gradle User Home has not been fully restored, so that the configuration-cache
* entry is not reusable.
*/
async restore(listener: CacheListener): Promise<void> {
if (listener.fullyRestored) {
return super.restore(listener)
}
core.info('Not restoring configuration-cache state, as Gradle User Home was not fully restored')
for (const cacheEntry of this.loadExtractedCacheEntries()) {
listener.entry(cacheEntry.pattern).markRequested('NOT_RESTORED')
}
}
/**
* Extract cache entries for the configuration cache in each project.
*/
protected getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[] {
return this.getProjectRoots().map(projectRoot => {
const configCachePath = path.resolve(projectRoot, '.gradle/configuration-cache')
return new ExtractedCacheEntryDefinition(
'configuration-cache',
configCachePath,
true
).withNonUniqueFileNames()
})
}
/**
* For every Gradle invocation, we record the project root directory. This method returns the entire
* set of project roots, to allow saving of configuration-cache entries for each.
*/
private getProjectRoots(): string[] {
const projectList = path.resolve(this.gradleUserHome, PROJECT_ROOTS_FILE)
if (!fs.existsSync(projectList)) {
core.info(`Missing project list file ${projectList}`)
return []
}
const projectRoots = fs.readFileSync(projectList, 'utf-8')
core.info(`Found project roots '${projectRoots}' in ${projectList}`)
return projectRoots.trim().split('\n')
}
}

View file

@ -1,395 +0,0 @@
import path from 'path'
import fs from 'fs'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import * as exec from '@actions/exec'
import {AbstractCache, META_FILE_DIR} from './cache-base'
import {CacheEntryListener, CacheListener} from './cache-reporting'
import {getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils'
const META_FILE = 'cache-metadata.json'
const INCLUDE_PATHS_PARAMETER = 'gradle-home-cache-includes'
const EXCLUDE_PATHS_PARAMETER = 'gradle-home-cache-excludes'
const EXTRACTED_CACHE_ENTRIES_PARAMETER = 'gradle-home-extracted-cache-entries'
/**
* Represents the result of attempting to load or store an extracted cache entry.
* An undefined cacheKey indicates that the operation did not succeed.
* The collected results are then used to populate the `cache-metadata.json` file for later use.
*/
class ExtractedCacheEntry {
artifactType: string
pattern: string
cacheKey: string | undefined
constructor(artifactType: string, pattern: string, cacheKey: string | undefined) {
this.artifactType = artifactType
this.pattern = pattern
this.cacheKey = cacheKey
}
}
/**
* Representation of all of the extracted cache entries for this Gradle User Home.
* This object is persisted to JSON file in the Gradle User Home directory for storing,
* and subsequently used to restore the Gradle User Home.
*/
class ExtractedCacheEntryMetadata {
entries: ExtractedCacheEntry[] = []
}
/**
* Caches and restores the entire Gradle User Home directory, extracting entries containing common artifacts
* for more efficient storage.
*/
export class GradleUserHomeCache extends AbstractCache {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'gradle', 'Gradle User Home')
}
/**
* Restore any extracted cache entries after the main Gradle User Home entry is restored.
*/
async afterRestore(listener: CacheListener): Promise<void> {
await this.debugReportGradleUserHomeSize('as restored from cache')
await this.restoreExtractedCacheEntries(listener)
await this.debugReportGradleUserHomeSize('after restoring common artifacts')
}
/**
* Restores any artifacts that were cached separately, based on the information in the `cache-metadata.json` file.
* Each extracted cache entry is restored in parallel, except when debugging is enabled.
*/
private async restoreExtractedCacheEntries(listener: CacheListener): Promise<void> {
const extractedCacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
const previouslyExtractedCacheEntries = this.loadExtractedCacheEntries()
const processes: Promise<ExtractedCacheEntry>[] = []
for (const cacheEntry of previouslyExtractedCacheEntries) {
const artifactType = cacheEntry.artifactType
const entryListener = listener.entry(cacheEntry.pattern)
// Handle case where the extracted-cache-entry definitions have been changed
if (extractedCacheEntryDefinitions.get(artifactType) === undefined) {
core.info(`Found extracted cache entry for ${artifactType} but no such entry defined`)
entryListener.markRequested('EXTRACTED_ENTRY_NOT_DEFINED')
} else {
processes.push(
this.restoreExtractedCacheEntry(
artifactType,
cacheEntry.cacheKey!,
cacheEntry.pattern,
entryListener
)
)
}
}
this.saveMetadataForCacheResults(await this.collectCacheResults(processes))
}
private async restoreExtractedCacheEntry(
artifactType: string,
cacheKey: string,
pattern: string,
listener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
listener.markRequested(cacheKey)
const restoredEntry = await this.restoreCache([pattern], cacheKey)
if (restoredEntry) {
core.info(`Restored ${artifactType} with key ${cacheKey} to ${pattern}`)
listener.markRestored(restoredEntry.key, restoredEntry.size)
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
} else {
core.info(`Did not restore ${artifactType} with key ${cacheKey} to ${pattern}`)
return new ExtractedCacheEntry(artifactType, pattern, undefined)
}
}
/**
* Extract and save any defined extracted cache entries prior to the main Gradle User Home entry being saved.
*/
async beforeSave(listener: CacheListener): Promise<void> {
await this.debugReportGradleUserHomeSize('before saving common artifacts')
this.removeExcludedPaths()
await this.saveExtractedCacheEntries(listener)
await this.debugReportGradleUserHomeSize(
"after saving common artifacts (only 'caches' and 'notifications' will be stored)"
)
}
/**
* Delete any file paths that are excluded by the `gradle-home-cache-excludes` parameter.
*/
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)
}
}
/**
* Saves any artifacts that are configured to be cached separately, based on the extracted cache entry definitions.
* These definitions are normally fixed, but can be overridden by the `gradle-home-extracted-cache-entries` parameter.
* Each entry is extracted and saved in parallel, except when debugging is enabled.
*/
private async saveExtractedCacheEntries(listener: CacheListener): Promise<void> {
// Load the cache entry definitions (from config) and the previously restored entries (from filesystem)
const cacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
const previouslyRestoredEntries = this.loadExtractedCacheEntries()
const cacheActions: Promise<ExtractedCacheEntry>[] = []
for (const [artifactType, pattern] of cacheEntryDefinitions) {
// Find all matching files for this cache entry definition
const globber = await glob.create(pattern, {
implicitDescendants: false,
followSymbolicLinks: false
})
const matchingFiles = await globber.glob()
if (matchingFiles.length === 0) {
this.debug(`No files found to cache for ${artifactType}`)
continue
}
if (this.isBundlePattern(pattern)) {
// For an extracted "bundle", use the defined pattern and cache all matching files in a single entry.
cacheActions.push(
this.saveExtractedCacheEntry(
matchingFiles,
artifactType,
pattern,
previouslyRestoredEntries,
listener.entry(pattern)
)
)
} else {
// Otherwise cache each matching file in a separate entry, using the complete file path as the cache pattern.
for (const cacheFile of matchingFiles) {
cacheActions.push(
this.saveExtractedCacheEntry(
[cacheFile],
artifactType,
cacheFile,
previouslyRestoredEntries,
listener.entry(cacheFile)
)
)
}
}
}
this.saveMetadataForCacheResults(await this.collectCacheResults(cacheActions))
}
private async saveExtractedCacheEntry(
matchingFiles: string[],
artifactType: string,
pattern: string,
previouslyRestoredEntries: ExtractedCacheEntry[],
entryListener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
const cacheKey = this.createCacheKeyForArtifacts(artifactType, matchingFiles)
const previouslyRestoredKey = previouslyRestoredEntries.find(
x => x.artifactType === artifactType && x.pattern === pattern
)?.cacheKey
if (previouslyRestoredKey === cacheKey) {
this.debug(`No change to previously restored ${artifactType}. Not saving.`)
} else {
core.info(`Caching ${artifactType} with path '${pattern}' and cache key: ${cacheKey}`)
const savedEntry = await this.saveCache([pattern], cacheKey)
if (savedEntry !== undefined) {
entryListener.markSaved(savedEntry.key, savedEntry.size)
}
}
for (const file of matchingFiles) {
tryDelete(file)
}
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
}
protected createCacheKeyForArtifacts(artifactType: 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 ${artifactType} from files: ${relativeFiles}`)
return `${cacheKeyPrefix}${artifactType}-${key}`
}
private isBundlePattern(pattern: string): boolean {
return pattern.endsWith('*')
}
private async collectCacheResults(processes: Promise<ExtractedCacheEntry>[]): Promise<ExtractedCacheEntry[]> {
// Run cache actions sequentially when debugging enabled
if (this.cacheDebuggingEnabled) {
for (const p of processes) {
await p
}
}
return await Promise.all(processes)
}
/**
* Load information about the extracted cache entries previously restored/saved. This is loaded from the 'cache-metadata.json' file.
*/
private loadExtractedCacheEntries(): ExtractedCacheEntry[] {
const cacheMetadataFile = path.resolve(this.gradleUserHome, META_FILE_DIR, META_FILE)
if (!fs.existsSync(cacheMetadataFile)) {
return []
}
const filedata = fs.readFileSync(cacheMetadataFile, 'utf-8')
core.debug(`Loaded cache metadata: ${filedata}`)
const extractedCacheEntryMetadata = JSON.parse(filedata) as ExtractedCacheEntryMetadata
return extractedCacheEntryMetadata.entries
}
/**
* Saves information about the extracted cache entries into the 'cache-metadata.json' file.
*/
private saveMetadataForCacheResults(results: ExtractedCacheEntry[]): void {
const extractedCacheEntryMetadata = new ExtractedCacheEntryMetadata()
extractedCacheEntryMetadata.entries = results.filter(x => x.cacheKey !== undefined)
const filedata = JSON.stringify(extractedCacheEntryMetadata)
core.debug(`Saving cache metadata: ${filedata}`)
const actionMetadataDirectory = path.resolve(this.gradleUserHome, META_FILE_DIR)
const cacheMetadataFile = path.resolve(actionMetadataDirectory, META_FILE)
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
fs.writeFileSync(cacheMetadataFile, filedata, 'utf-8')
}
/**
* Determines the paths within Gradle User Home to cache.
* By default, this is the 'caches' and 'notifications' directories,
* but this can be overridden by the `gradle-home-cache-includes` parameter.
*/
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)
}
/**
* Return the extracted cache entry definitions, which determine which artifacts will be cached
* separately from the rest of the Gradle User Home cache entry.
* This is normally a fixed set, but can be overridden by the `gradle-home-extracted-cache-entries` parameter.
*/
private getExtractedCacheEntryDefinitions(): Map<string, string> {
const rawDefinitions = core.getInput(EXTRACTED_CACHE_ENTRIES_PARAMETER)
const parsedDefinitions = JSON.parse(rawDefinitions)
return new Map(Array.from(parsedDefinitions, ([key, value]) => [key, path.resolve(this.gradleUserHome, value)]))
}
/**
* When cache debugging is enabled, this method will give a detailed report
* of the Gradle User Home contents.
*/
private async debugReportGradleUserHomeSize(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('-----------------------')
}
protected initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
const propertiesFile = path.resolve(gradleUserHome, 'gradle.properties')
fs.writeFileSync(propertiesFile, 'org.gradle.daemon=false')
const buildScanCapture = path.resolve(initScriptsDir, 'build-scan-capture.init.gradle')
fs.writeFileSync(
buildScanCapture,
`import org.gradle.util.GradleVersion
// Only run again root build. Do not run against included builds.
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 buildOutcome = ""
def scanFile = new File("gradle-build-scan.txt")
buildFinished { result ->
buildOutcome = result.failure == null ? " succeeded" : " failed"
}
buildScanPublished { buildScan ->
scanFile.text = buildScan.buildScanUri
// Send commands directly to GitHub Actions via STDOUT.
def message = "Build '\${rootProjectName}'\${buildOutcome} - \${buildScan.buildScanUri}"
println("::notice ::\${message}")
println("::set-output name=build-scan-url::\${buildScan.buildScanUri}")
}
}
}`
)
}
}

View file

@ -1,52 +0,0 @@
import * as core from '@actions/core'
import path from 'path'
import fs from 'fs'
import {AbstractCache, META_FILE_DIR, PROJECT_ROOTS_FILE} from './cache-base'
/**
* A simple cache that saves and restores the '.gradle/configuration-cache' directory in the project root.
*/
export class ProjectDotGradleCache extends AbstractCache {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'project', 'Project configuration cache')
}
protected getCachePath(): string[] {
return this.getProjectRoots().map(x => path.resolve(x, '.gradle/configuration-cache'))
}
protected initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
const projectRootCapture = path.resolve(initScriptsDir, 'project-root-capture.init.gradle')
fs.writeFileSync(
projectRootCapture,
`
// Only run again root build. Do not run against included builds.
def isTopLevelBuild = gradle.getParent() == null
if (isTopLevelBuild) {
settingsEvaluated { settings ->
def projectRootEntry = settings.rootDir.absolutePath + "\\n"
def projectRootList = new File(settings.gradle.gradleUserHomeDir, "${META_FILE_DIR}/${PROJECT_ROOTS_FILE}")
println "Adding " + projectRootEntry + " to " + projectRootList
if (!projectRootList.exists() || !projectRootList.text.contains(projectRootEntry)) {
projectRootList << projectRootEntry
}
}
}`
)
}
/**
* For every Gradle invocation, we record the project root directory. This method returns the entire
* set of project roots, to allow saving of configuration-cache entries for each.
*/
private getProjectRoots(): string[] {
const projectList = path.resolve(this.gradleUserHome, META_FILE_DIR, PROJECT_ROOTS_FILE)
if (!fs.existsSync(projectList)) {
core.info(`Missing project list file ${projectList}`)
return []
}
const projectRoots = fs.readFileSync(projectList, 'utf-8')
core.info(`Found project roots '${projectRoots}' in ${projectList}`)
return projectRoots.trim().split('\n')
}
}

View file

@ -46,6 +46,36 @@ export function hashStrings(values: string[]): string {
return hash.digest('hex') return hash.digest('hex')
} }
export async function restoreCache(
cachePath: string[],
cacheKey: string,
cacheRestoreKeys: string[] = []
): Promise<cache.CacheEntry | undefined> {
try {
return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys)
} catch (error) {
handleCacheFailure(error, `Failed to restore ${cacheKey}`)
return undefined
}
}
export async function saveCache(cachePath: string[], cacheKey: string): Promise<cache.CacheEntry | undefined> {
try {
return await cache.saveCache(cachePath, cacheKey)
} catch (error) {
handleCacheFailure(error, `Failed to save cache entry ${cacheKey}`)
}
return undefined
}
export function cacheDebug(message: string): void {
if (isCacheDebuggingEnabled()) {
core.info(message)
} else {
core.debug(message)
}
}
export function handleCacheFailure(error: unknown, message: string): void { export function handleCacheFailure(error: unknown, message: string): void {
if (error instanceof cache.ValidationError) { if (error instanceof cache.ValidationError) {
// Fail on cache validation errors // Fail on cache validation errors

View file

@ -1,8 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {GradleUserHomeCache} from './cache-gradle-user-home'
import {ProjectDotGradleCache} from './cache-project-dot-gradle'
import {isCacheDisabled, isCacheReadOnly} from './cache-utils' import {isCacheDisabled, isCacheReadOnly} from './cache-utils'
import {logCachingReport, CacheListener} from './cache-reporting' import {logCachingReport, CacheListener} from './cache-reporting'
import {GradleStateCache} from './cache-base'
const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED' const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED'
const GRADLE_USER_HOME = 'GRADLE_USER_HOME' const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
@ -13,29 +12,22 @@ export async function restore(gradleUserHome: string): Promise<void> {
return return
} }
const gradleUserHomeCache = new GradleUserHomeCache(gradleUserHome) const gradleStateCache = new GradleStateCache(gradleUserHome)
gradleUserHomeCache.init() gradleStateCache.init()
const projectDotGradleCache = new ProjectDotGradleCache(gradleUserHome)
projectDotGradleCache.init()
await core.group('Restore Gradle state from cache', async () => { await core.group('Restore Gradle state from cache', async () => {
core.saveState(GRADLE_USER_HOME, gradleUserHome) core.saveState(GRADLE_USER_HOME, gradleUserHome)
const cacheListener = new CacheListener() const cacheListener = new CacheListener()
await gradleUserHomeCache.restore(cacheListener) await gradleStateCache.restore(cacheListener)
if (cacheListener.fullyRestored) {
// Only restore the configuration-cache if the Gradle Home is fully restored
await projectDotGradleCache.restore(cacheListener)
} else {
// Otherwise, prepare the cache key for later save()
core.info('Gradle Home cache not fully restored: not restoring configuration-cache state')
projectDotGradleCache.prepareCacheKey()
}
core.saveState(CACHE_LISTENER, cacheListener.stringify()) core.saveState(CACHE_LISTENER, cacheListener.stringify())
}) })
// Export var that is detected in all later restore steps
core.exportVariable(CACHE_RESTORED_VAR, true)
// Export state that is detected in corresponding post-action step
core.saveState(CACHE_RESTORED_VAR, true)
} }
export async function save(): Promise<void> { export async function save(): Promise<void> {
@ -53,10 +45,7 @@ export async function save(): Promise<void> {
await core.group('Caching Gradle state', async () => { await core.group('Caching Gradle state', async () => {
const gradleUserHome = core.getState(GRADLE_USER_HOME) const gradleUserHome = core.getState(GRADLE_USER_HOME)
return Promise.all([ return new GradleStateCache(gradleUserHome).save(cacheListener)
new GradleUserHomeCache(gradleUserHome).save(cacheListener),
new ProjectDotGradleCache(gradleUserHome).save(cacheListener)
])
}) })
logCachingReport(cacheListener) logCachingReport(cacheListener)
@ -72,11 +61,6 @@ function shouldRestoreCaches(): boolean {
core.info('Cache only restored on first action step.') core.info('Cache only restored on first action step.')
return false return false
} }
// Export var that is detected in all later restore steps
core.exportVariable(CACHE_RESTORED_VAR, true)
// Export state that is detected in corresponding post-action step
core.saveState(CACHE_RESTORED_VAR, true)
return true return true
} }